oasis_test_v2 2.2.80 → 2.2.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +971 -74
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -218387,6 +218387,8 @@ var init_memory_control_plane_store = __esm({
218387
218387
  // key = companyId\0type\0id
218388
218388
  offboardings = /* @__PURE__ */ new Map();
218389
218389
  // key = companyId\0operationId
218390
+ /** 存量身份映射审计(ADR D3.4):**只追加**的一条数组,插入序即写入序。 */
218391
+ roleMigrations = [];
218390
218392
  // —— 公司 ——
218391
218393
  async createCompany(c) {
218392
218394
  if (this.companies.has(c.id)) throw new Error(`company already exists: ${c.id}`);
@@ -218418,8 +218420,25 @@ var init_memory_control_plane_store = __esm({
218418
218420
  }
218419
218421
  if (patch.status !== void 0) c.status = patch.status;
218420
218422
  if (patch.onboardingVariant !== void 0) c.onboardingVariant = patch.onboardingVariant;
218423
+ if (patch.dissolvedAt !== void 0) {
218424
+ if (patch.dissolvedAt === null) delete c.dissolvedAt;
218425
+ else c.dissolvedAt = patch.dissolvedAt;
218426
+ }
218427
+ if (patch.dissolvedBy !== void 0) {
218428
+ if (patch.dissolvedBy === null) delete c.dissolvedBy;
218429
+ else c.dissolvedBy = patch.dissolvedBy;
218430
+ }
218421
218431
  this.companies.set(id, c);
218422
218432
  }
218433
+ async dissolveCompanyIfOwner(companyId, accountId, at2) {
218434
+ const c = this.companies.get(companyId);
218435
+ if (!c) return { ok: false, reason: "not_found" };
218436
+ const caller = this.members.get(memberKey(companyId, accountId));
218437
+ if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
218438
+ const next = { ...c, status: "suspended", dissolvedAt: at2, dissolvedBy: accountId };
218439
+ this.companies.set(companyId, next);
218440
+ return { ok: true, company: { ...next } };
218441
+ }
218423
218442
  // —— 成员 ——
218424
218443
  async upsertMember(m2) {
218425
218444
  const key2 = memberKey(m2.companyId, m2.accountId);
@@ -218476,6 +218495,41 @@ var init_memory_control_plane_store = __esm({
218476
218495
  this.members.set(key2, updated);
218477
218496
  return { ok: true, member: { ...updated } };
218478
218497
  }
218498
+ async setSoleCompanyOwner(companyId, toAccountId, opts) {
218499
+ const target = this.members.get(memberKey(companyId, toAccountId));
218500
+ if (!target) return { ok: false, reason: "not_found" };
218501
+ if (opts?.requireCurrentOwner !== void 0) {
218502
+ const caller = this.members.get(memberKey(companyId, opts.requireCurrentOwner));
218503
+ if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
218504
+ }
218505
+ if (!target.onboardingCompletedAt && target.role !== "owner") {
218506
+ return { ok: false, reason: "target_not_listed" };
218507
+ }
218508
+ const acc = this.accounts.get(toAccountId);
218509
+ if (acc && acc.status !== "active") return { ok: false, reason: "target_inactive" };
218510
+ const others = [];
218511
+ for (const m2 of this.members.values()) {
218512
+ if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
218513
+ }
218514
+ if (others.length === 0 && target.role === "owner") {
218515
+ return { ok: true, owner: { ...target }, demoted: [] };
218516
+ }
218517
+ const demoted = others.map((m2) => {
218518
+ const next = { ...m2, role: "admin" };
218519
+ this.members.set(memberKey(companyId, m2.accountId), next);
218520
+ return { ...next };
218521
+ });
218522
+ const owner = { ...target, role: "owner" };
218523
+ this.members.set(memberKey(companyId, toAccountId), owner);
218524
+ return { ok: true, owner: { ...owner }, demoted };
218525
+ }
218526
+ // —— 存量身份映射审计(ADR D3.4):只追加 ——
218527
+ async appendMemberRoleMigrations(rows) {
218528
+ for (const r of rows) this.roleMigrations.push({ ...r });
218529
+ }
218530
+ async listMemberRoleMigrations(companyId) {
218531
+ return this.roleMigrations.filter((r) => companyId === void 0 || r.companyId === companyId).map((r) => ({ ...r }));
218532
+ }
218479
218533
  // —— 邀请 ——
218480
218534
  async createInvitation(inv) {
218481
218535
  if (this.invitations.has(inv.id)) throw new Error(`invitation already exists: ${inv.id}`);
@@ -223753,9 +223807,13 @@ var init_control_plane_file_store = __esm({
223753
223807
  MUTATORS2 = [
223754
223808
  "createCompany",
223755
223809
  "updateCompany",
223810
+ "dissolveCompanyIfOwner",
223756
223811
  "upsertMember",
223757
223812
  "removeMember",
223758
223813
  "removeMemberIfNotLastOwner",
223814
+ "updateMemberRoleIfNotLastOwner",
223815
+ "setSoleCompanyOwner",
223816
+ "appendMemberRoleMigrations",
223759
223817
  "createInvitation",
223760
223818
  "updateInvitationStatus",
223761
223819
  "updateInvitationStatusIf",
@@ -223807,6 +223865,7 @@ var init_control_plane_file_store = __esm({
223807
223865
  for (const c of snap.agentCredentials ?? []) await MemoryControlPlaneStore.prototype.putAgentCredential.call(store, c);
223808
223866
  for (const b2 of snap.principalBridges ?? []) await MemoryControlPlaneStore.prototype.putPrincipalBridge.call(store, b2);
223809
223867
  for (const op of snap.offboardings ?? []) store.offboardings.set(`${op.companyId}\0${op.operationId}`, op);
223868
+ for (const r of snap.roleMigrations ?? []) store.roleMigrations.push({ ...r });
223810
223869
  if (needsOnboardingBackfill) await store.save();
223811
223870
  } else {
223812
223871
  fs25.mkdirSync(path24.dirname(file), { recursive: true });
@@ -223844,7 +223903,8 @@ var init_control_plane_file_store = __esm({
223844
223903
  agentCredentials,
223845
223904
  principalBridges,
223846
223905
  // 交接操作无按公司列举的读面(按 operationId / accountId 定位),直接读 protected map。
223847
- offboardings: [...this.offboardings.values()]
223906
+ offboardings: [...this.offboardings.values()],
223907
+ roleMigrations: await this.listMemberRoleMigrations()
223848
223908
  };
223849
223909
  fs25.writeFileSync(this.file, JSON.stringify(snap, null, 2));
223850
223910
  }
@@ -225071,6 +225131,197 @@ var init_org_model_backfill = __esm({
225071
225131
  }
225072
225132
  });
225073
225133
 
225134
+ // ../server/src/org-identity-migration.ts
225135
+ function e1Candidate(members) {
225136
+ if (members.length === 0) return void 0;
225137
+ if (members.some((m2) => !m2.createdAt)) return void 0;
225138
+ return [...members].sort((a, b2) => {
225139
+ const ta = a.createdAt;
225140
+ const tb = b2.createdAt;
225141
+ if (ta !== tb) return ta < tb ? -1 : 1;
225142
+ return a.accountId < b2.accountId ? -1 : a.accountId > b2.accountId ? 1 : 0;
225143
+ })[0];
225144
+ }
225145
+ async function inventoryOrgIdentity(deps, opts = {}) {
225146
+ const listMembers = deps.listMembersWithCreatedAt ?? ((cid) => deps.store.listMembers(cid));
225147
+ const companies = (await deps.store.listCompanies()).filter((c) => opts.companyId === void 0 || c.id === opts.companyId);
225148
+ const rows = [];
225149
+ for (const c of companies) {
225150
+ const members = await listMembers(c.id);
225151
+ rows.push(decide(c, members, opts.manifest));
225152
+ }
225153
+ return {
225154
+ rows,
225155
+ blocked: rows.filter((r) => r.decision.startsWith("blocked")).length,
225156
+ pending: rows.filter((r) => (r.decision === "remap" || r.decision === "manifest") && r.changes.length > 0).length
225157
+ };
225158
+ }
225159
+ function decide(company, members, manifest) {
225160
+ const owners = members.filter((m2) => m2.role === "owner");
225161
+ const base = {
225162
+ companyId: company.id,
225163
+ companyName: company.name,
225164
+ status: company.status,
225165
+ members: members.length,
225166
+ owners: owners.length,
225167
+ admins: members.filter((m2) => m2.role === "admin").length,
225168
+ plainMembers: members.filter((m2) => m2.role === "member").length,
225169
+ rowsWithoutCreatedAt: members.filter((m2) => !m2.createdAt).length,
225170
+ changes: []
225171
+ };
225172
+ if (members.length === 0) {
225173
+ return { ...base, decision: "blocked:empty", reason: "\u7EC4\u7EC7\u6CA1\u6709\u4EFB\u4F55\u6210\u5458\u884C\u2014\u2014\u5220\u8FD8\u662F\u7559\u7531\u4EBA\u51B3\u5B9A\uFF0C\u811A\u672C\u4E0D\u78B0" };
225174
+ }
225175
+ const pinned = manifest?.owners[company.id];
225176
+ if (pinned !== void 0) {
225177
+ if (!members.some((m2) => m2.accountId === pinned)) {
225178
+ return {
225179
+ ...base,
225180
+ decision: "blocked:no-owner",
225181
+ reason: `manifest \u6307\u5B9A\u7684 ${pinned} \u4E0D\u662F\u8BE5\u7EC4\u7EC7\u73B0\u5B58\u6210\u5458\u884C\u2014\u2014\u6307\u9519\u4EBA\u6BD4\u4E0D\u6307\u66F4\u7CDF\uFF0C\u62D2\u7EDD\u6267\u884C`
225182
+ };
225183
+ }
225184
+ return {
225185
+ ...base,
225186
+ decision: "manifest",
225187
+ targetOwnerAccountId: pinned,
225188
+ changes: plannedChanges(members, pinned),
225189
+ reason: `\u6309\u51BB\u7ED3 manifest\uFF08${manifest.manifestVersion}\uFF09\u6307\u5B9A ${pinned} \u4E3A\u7EC4\u7EC7\u7BA1\u7406\u5458`
225190
+ };
225191
+ }
225192
+ const e1 = e1Candidate(members);
225193
+ if (!e1) {
225194
+ return {
225195
+ ...base,
225196
+ decision: "blocked:no-created-at",
225197
+ reason: `\u6709 ${base.rowsWithoutCreatedAt} \u884C\u6210\u5458\u6CA1\u6709 created_at\uFF08\u8BE5\u5217\u662F expand \u671F\u624D\u52A0\u7684\uFF09\uFF0CE1 \u4E0D\u6210\u7ACB \u2192 \u5199\u8FDB manifest \u7531\u4EBA\u6307\u5B9A\u521B\u5EFA\u8005`
225198
+ };
225199
+ }
225200
+ if (e1.role === "member") {
225201
+ return {
225202
+ ...base,
225203
+ decision: "blocked:e1-is-plain-member",
225204
+ e1AccountId: e1.accountId,
225205
+ reason: `\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId} \u5F53\u524D\u662F\u666E\u901A\u6210\u5458\uFF08\u53EF\u80FD\u88AB\u964D\u7EA7\u8FC7\uFF0C\u6216\u6570\u636E\u6765\u81EA\u8FC1\u79FB\u6307\u6D3E\uFF09\u2192 \u7531\u4EBA\u6307\u5B9A`
225206
+ };
225207
+ }
225208
+ if (owners.length === 0) {
225209
+ return {
225210
+ ...base,
225211
+ decision: "blocked:no-owner",
225212
+ e1AccountId: e1.accountId,
225213
+ reason: "\u4E00\u4E2A owner \u90FD\u6CA1\u6709 \u2192 \u7531\u4EBA\u6307\u5B9A\uFF08\u8C01\u5F53\u552F\u4E00\u6301\u6709\u4EBA\u662F\u4E0D\u53EF\u9006\u7684\u4EBA\u4E8B\u51B3\u5B9A\uFF09"
225214
+ };
225215
+ }
225216
+ const changes = plannedChanges(members, e1.accountId);
225217
+ if (changes.length === 0) {
225218
+ return {
225219
+ ...base,
225220
+ decision: "ok",
225221
+ e1AccountId: e1.accountId,
225222
+ targetOwnerAccountId: e1.accountId,
225223
+ reason: "\u5DF2\u7ECF\u6070\u6709\u4E00\u4F4D\u7EC4\u7EC7\u7BA1\u7406\u5458\u3001\u4E14\u5C31\u662F\u6700\u65E9\u52A0\u5165\u7684\u90A3\u4F4D\u2014\u2014\u65E0\u9700\u6539\u52A8"
225224
+ };
225225
+ }
225226
+ return {
225227
+ ...base,
225228
+ decision: "remap",
225229
+ e1AccountId: e1.accountId,
225230
+ targetOwnerAccountId: e1.accountId,
225231
+ changes,
225232
+ reason: `E1\uFF1A\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId}\uFF08\u5F53\u524D ${e1.role}\uFF09\u2192 \u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u5176\u4F59\u539F owner \u964D\u4E3A\u7BA1\u7406\u5458`
225233
+ };
225234
+ }
225235
+ function plannedChanges(members, targetOwner) {
225236
+ const out = [];
225237
+ for (const m2 of members) {
225238
+ if (m2.accountId === targetOwner) {
225239
+ if (m2.role !== "owner") out.push({ accountId: m2.accountId, roleBefore: m2.role, roleAfter: "owner" });
225240
+ continue;
225241
+ }
225242
+ if (m2.role === "owner") out.push({ accountId: m2.accountId, roleBefore: "owner", roleAfter: "admin" });
225243
+ }
225244
+ return out;
225245
+ }
225246
+ async function backfillOrgIdentity(deps, opts = {}) {
225247
+ const log3 = opts.log ?? (() => {
225248
+ });
225249
+ const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
225250
+ const inv = await inventoryOrgIdentity(deps, opts);
225251
+ let applied = 0;
225252
+ let auditRows = 0;
225253
+ for (const row of inv.rows) {
225254
+ if (row.decision !== "remap" && row.decision !== "manifest") {
225255
+ if (row.decision.startsWith("blocked")) log3(` \u26D4 ${row.companyId} ${row.decision}\uFF1A${row.reason}`);
225256
+ continue;
225257
+ }
225258
+ if (row.changes.length === 0) continue;
225259
+ const target = row.targetOwnerAccountId;
225260
+ const desc = row.changes.map((c) => `${c.accountId} ${c.roleBefore}\u2192${c.roleAfter}`).join("\uFF0C");
225261
+ log3(` ${row.companyId}\uFF1A${desc}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225262
+ if (!opts.apply) continue;
225263
+ const res = await deps.store.setSoleCompanyOwner(row.companyId, target);
225264
+ if (!res.ok) {
225265
+ throw new Error(
225266
+ `${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08target_not_listed = \u90A3\u4E2A\u4EBA\u8FD8\u6CA1\u8D70\u5B8C\u65B0\u624B\u6307\u5F15\uFF1Btarget_inactive = \u8D26\u53F7\u5DF2\u505C\u7528\uFF09`
225267
+ );
225268
+ }
225269
+ applied += 1;
225270
+ const at2 = now();
225271
+ const evidence = row.decision === "manifest" ? "manifest" : "E1";
225272
+ const rows = res.demoted.map((m2) => ({
225273
+ companyId: row.companyId,
225274
+ accountId: m2.accountId,
225275
+ roleBefore: "owner",
225276
+ roleAfter: "admin",
225277
+ evidence,
225278
+ ...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
225279
+ appliedAt: at2
225280
+ }));
225281
+ const promoted = row.changes.find((c) => c.accountId === target);
225282
+ if (promoted) {
225283
+ rows.push({
225284
+ companyId: row.companyId,
225285
+ accountId: target,
225286
+ roleBefore: promoted.roleBefore,
225287
+ roleAfter: "owner",
225288
+ evidence,
225289
+ ...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
225290
+ appliedAt: at2
225291
+ });
225292
+ }
225293
+ await deps.store.appendMemberRoleMigrations(rows);
225294
+ auditRows += rows.length;
225295
+ }
225296
+ return { ...inv, applied, auditRows };
225297
+ }
225298
+ async function rollbackOrgIdentity(deps, opts) {
225299
+ const log3 = opts.log ?? (() => {
225300
+ });
225301
+ const audit2 = await deps.store.listMemberRoleMigrations(opts.companyId);
225302
+ const plan = [];
225303
+ let reverted = 0;
225304
+ for (const r of [...audit2].reverse()) {
225305
+ const member = await deps.store.getMember(r.companyId, r.accountId);
225306
+ if (!member) {
225307
+ log3(` \u8DF3\u8FC7 ${r.accountId}\uFF1A\u6210\u5458\u884C\u5DF2\u4E0D\u5B58\u5728\uFF08\u88AB\u79FB\u9664\u8FC7\uFF09`);
225308
+ continue;
225309
+ }
225310
+ if (member.role === r.roleBefore) continue;
225311
+ plan.push({ companyId: r.companyId, accountId: r.accountId, from: member.role, to: r.roleBefore });
225312
+ log3(` ${r.companyId} ${r.accountId} ${member.role}\u2192${r.roleBefore}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225313
+ if (!opts.apply) continue;
225314
+ await deps.store.upsertMember({ ...member, role: r.roleBefore });
225315
+ reverted += 1;
225316
+ }
225317
+ return { plan, reverted };
225318
+ }
225319
+ var init_org_identity_migration = __esm({
225320
+ "../server/src/org-identity-migration.ts"() {
225321
+ "use strict";
225322
+ }
225323
+ });
225324
+
225074
225325
  // ../server/src/config-audit.ts
225075
225326
  function shallowDiff(before, after) {
225076
225327
  const out = {};
@@ -232382,6 +232633,68 @@ var init_runtime_tenancy = __esm({
232382
232633
  }
232383
232634
  });
232384
232635
 
232636
+ // ../server/src/governance/agent-manage-guard.ts
232637
+ function isCompanyManager(role) {
232638
+ return role === "owner" || role === "admin";
232639
+ }
232640
+ function isAssistantAgent(subject) {
232641
+ return subject.positions.includes(ASSISTANT_POSITION);
232642
+ }
232643
+ function mayManageAgent(caller, subject) {
232644
+ if (!isAssistantAgent(subject)) return isCompanyManager(caller.role);
232645
+ if (subject.boundHumanActorId === void 0) return isCompanyManager(caller.role);
232646
+ if (subject.boundHumanActorId === caller.actorId) return true;
232647
+ if (subject.boundHumanReportsTo !== void 0 && subject.boundHumanReportsTo === caller.actorId) return true;
232648
+ return isCompanyManager(caller.role);
232649
+ }
232650
+ async function assertMayManageAgent(guard, ctx, agent) {
232651
+ if (!guard) return;
232652
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232653
+ const subject = { positions: agent.positions };
232654
+ if (isAssistantAgent(subject)) {
232655
+ const human = await guard.boundHuman(ctx.companyId, agent.id);
232656
+ if (human) {
232657
+ subject.boundHumanActorId = human;
232658
+ subject.boundHumanReportsTo = await guard.reportsTo(ctx.companyId, human) ?? void 0;
232659
+ }
232660
+ }
232661
+ if (mayManageAgent({ role, actorId: ctx.callerActorId }, subject)) return;
232662
+ throw new AgentManageForbiddenError(
232663
+ isAssistantAgent(subject) ? "\u53EA\u6709\u672C\u4EBA\u3001\u5176\u76F4\u63A5\u4E0A\u7EA7\u6216\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u8FD9\u4E2A\u52A9\u7406" : "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u4E13\u5BB6 Agent"
232664
+ );
232665
+ }
232666
+ async function assertCompanyManager(guard, ctx, what) {
232667
+ if (!guard) return;
232668
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232669
+ if (isCompanyManager(role)) return;
232670
+ throw new AgentManageForbiddenError(`\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5${what}`);
232671
+ }
232672
+ async function assertMayCreateAgent(guard, ctx) {
232673
+ return assertCompanyManager(guard, ctx, "\u65B0\u5EFA Agent");
232674
+ }
232675
+ async function assertMaySelfOrManage(guard, ctx, targetActorId) {
232676
+ if (!guard) return;
232677
+ if (ctx.callerActorId === targetActorId) return;
232678
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232679
+ if (isCompanyManager(role)) return;
232680
+ throw new AgentManageForbiddenError("\u53EA\u80FD\u4FEE\u6539\u81EA\u5DF1\u7684\u6863\u6848\uFF1B\u6539\u4ED6\u4EBA\u7684\u9700\u8981\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u6743\u9650");
232681
+ }
232682
+ var ASSISTANT_POSITION, AgentManageForbiddenError;
232683
+ var init_agent_manage_guard = __esm({
232684
+ "../server/src/governance/agent-manage-guard.ts"() {
232685
+ "use strict";
232686
+ ASSISTANT_POSITION = "assistant";
232687
+ AgentManageForbiddenError = class extends Error {
232688
+ status = 403;
232689
+ code = "FORBIDDEN";
232690
+ constructor(message) {
232691
+ super(message);
232692
+ this.name = "AgentManageForbiddenError";
232693
+ }
232694
+ };
232695
+ }
232696
+ });
232697
+
232385
232698
  // ../server/src/domains/actors/routes.ts
232386
232699
  async function asApiError(run) {
232387
232700
  try {
@@ -232470,6 +232783,27 @@ function mapMarketError(err) {
232470
232783
  }
232471
232784
  function actorsDomain(opts) {
232472
232785
  const { resolveCtx } = opts;
232786
+ const asForbidden = async (run) => {
232787
+ try {
232788
+ await run();
232789
+ } catch (e) {
232790
+ if (e instanceof AgentManageForbiddenError) throw new ApiError(e.status, e.code, e.message);
232791
+ throw e;
232792
+ }
232793
+ };
232794
+ const guardAgentWrite = async (req, actor) => {
232795
+ const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
232796
+ if (actor.kind !== "agent") {
232797
+ await asForbidden(() => assertMaySelfOrManage(opts.agentManageGuard, ctx, actor.id));
232798
+ return;
232799
+ }
232800
+ await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
232801
+ };
232802
+ const guardExistingActorWrite = async (req, service, actorId) => {
232803
+ const existing = await service.getActor(actorId);
232804
+ if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
232805
+ await guardAgentWrite(req, existing);
232806
+ };
232473
232807
  const revealLimits = opts.credentialRevealLimits ?? { perSession: 200, perSessionPerKey: 20 };
232474
232808
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
232475
232809
  const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
@@ -232522,6 +232856,10 @@ function actorsDomain(opts) {
232522
232856
  const { service } = await resolveCtx(req.auth.companyId);
232523
232857
  const b2 = req.body;
232524
232858
  if (!b2?.id || !b2.kind || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / kind / name");
232859
+ await asForbidden(() => assertMayCreateAgent(opts.agentManageGuard, {
232860
+ companyId: req.auth.companyId,
232861
+ callerActorId: req.auth.actor
232862
+ }));
232525
232863
  const { id, kind, name } = b2;
232526
232864
  const positions = positionsFromPayload(b2);
232527
232865
  const avatar = avatarRefFromPayload(b2);
@@ -232548,6 +232886,7 @@ function actorsDomain(opts) {
232548
232886
  const { service } = await resolveCtx(req.auth.companyId);
232549
232887
  const existing = await service.getActor(req.params.id);
232550
232888
  if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${req.params.id}`);
232889
+ await guardAgentWrite(req, existing);
232551
232890
  const b2 = req.body ?? {};
232552
232891
  const positions = positionsFromPayload(b2);
232553
232892
  const avatar = avatarRefFromPayload(b2);
@@ -232582,23 +232921,27 @@ function actorsDomain(opts) {
232582
232921
  });
232583
232922
  router.delete("/api/actors/:id", async (req) => {
232584
232923
  const { service } = await resolveCtx(req.auth.companyId);
232924
+ await guardExistingActorWrite(req, service, req.params.id);
232585
232925
  await service.disableActor(req.params.id, req.auth.actor);
232586
232926
  return { status: 200, body: { ok: true } };
232587
232927
  });
232588
232928
  router.post("/api/actors/:id/avatar", async (req) => {
232589
232929
  const { service, blobs } = await resolveCtx(req.auth.companyId);
232590
232930
  if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
232931
+ await guardExistingActorWrite(req, service, req.params.id);
232591
232932
  const ref2 = await storeImageBlob(blobs, req.body?.data);
232592
232933
  const actor = await service.setActorAvatar(req.params.id, ref2, req.auth.actor);
232593
232934
  return { status: 201, body: { avatar: actor.avatar } };
232594
232935
  });
232595
232936
  router.delete("/api/actors/:id/avatar", async (req) => {
232596
232937
  const { service } = await resolveCtx(req.auth.companyId);
232938
+ await guardExistingActorWrite(req, service, req.params.id);
232597
232939
  await service.setActorAvatar(req.params.id, null, req.auth.actor);
232598
232940
  return { status: 200, body: { ok: true } };
232599
232941
  });
232600
232942
  router.post("/api/teams/:id/icon", async (req) => {
232601
232943
  const { service, blobs } = await resolveCtx(req.auth.companyId);
232944
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
232602
232945
  if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
232603
232946
  const ref2 = await storeImageBlob(blobs, req.body?.data);
232604
232947
  await service.setTeamIcon(req.params.id, ref2);
@@ -232606,6 +232949,7 @@ function actorsDomain(opts) {
232606
232949
  });
232607
232950
  router.delete("/api/teams/:id/icon", async (req) => {
232608
232951
  const { service } = await resolveCtx(req.auth.companyId);
232952
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
232609
232953
  await service.setTeamIcon(req.params.id, null);
232610
232954
  return { status: 200, body: { ok: true } };
232611
232955
  });
@@ -232617,6 +232961,7 @@ function actorsDomain(opts) {
232617
232961
  });
232618
232962
  router.post("/api/actors/:id/config", async (req) => {
232619
232963
  const { service } = await resolveCtx(req.auth.companyId);
232964
+ await guardExistingActorWrite(req, service, req.params.id);
232620
232965
  const b2 = req.body ?? {};
232621
232966
  const checkString = (key2) => {
232622
232967
  if (b2[key2] !== void 0 && typeof b2[key2] !== "string") {
@@ -232678,6 +233023,7 @@ function actorsDomain(opts) {
232678
233023
  const { service } = await resolveCtx(req.auth.companyId);
232679
233024
  const actorId = req.params.id;
232680
233025
  if (!await service.getActor(actorId)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
233026
+ await guardExistingActorWrite(req, service, actorId);
232681
233027
  const raw = req.body?.candidates;
232682
233028
  if (!Array.isArray(raw)) throw new ApiError(400, "BAD_REQUEST", "candidates \u5FC5\u987B\u662F\u6570\u7EC4");
232683
233029
  if (raw.length > 20) throw new ApiError(400, "BAD_REQUEST", "\u5019\u9009\u8FD0\u884C\u65F6\u6700\u591A 20 \u6761");
@@ -232712,6 +233058,7 @@ function actorsDomain(opts) {
232712
233058
  });
232713
233059
  router.post("/api/actors/:id/config/rollback", async (req) => {
232714
233060
  const { service } = await resolveCtx(req.auth.companyId);
233061
+ await guardExistingActorWrite(req, service, req.params.id);
232715
233062
  const b2 = req.body;
232716
233063
  if (typeof b2?.version !== "number") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 version");
232717
233064
  const cfg = await service.rollbackConfig(req.params.id, b2.version, req.auth.actor);
@@ -232723,6 +233070,7 @@ function actorsDomain(opts) {
232723
233070
  });
232724
233071
  router.patch("/api/actors/:id/settings", async (req) => {
232725
233072
  const { service } = await resolveCtx(req.auth.companyId);
233073
+ await guardExistingActorWrite(req, service, req.params.id);
232726
233074
  const b2 = req.body ?? {};
232727
233075
  if (b2.memoryEnabled !== void 0 && typeof b2.memoryEnabled !== "boolean") {
232728
233076
  throw new ApiError(400, "BAD_REQUEST", "memoryEnabled \u5FC5\u987B\u662F boolean");
@@ -232742,6 +233090,7 @@ function actorsDomain(opts) {
232742
233090
  const { service } = await resolveCtx(req.auth.companyId);
232743
233091
  const b2 = req.body;
232744
233092
  if (!b2?.id || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / name");
233093
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u65B0\u5EFA\u56E2\u961F"));
232745
233094
  await service.upsertTeam({
232746
233095
  id: b2.id,
232747
233096
  name: b2.name,
@@ -232762,6 +233111,7 @@ function actorsDomain(opts) {
232762
233111
  const { service } = await resolveCtx(req.auth.companyId);
232763
233112
  const b2 = req.body;
232764
233113
  if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
233114
+ await guardExistingActorWrite(req, service, b2.actorId);
232765
233115
  const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
232766
233116
  const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
232767
233117
  if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
@@ -232770,6 +233120,7 @@ function actorsDomain(opts) {
232770
233120
  });
232771
233121
  router.delete("/api/bindings/:actorId", async (req) => {
232772
233122
  const { service } = await resolveCtx(req.auth.companyId);
233123
+ await guardExistingActorWrite(req, service, req.params.actorId);
232773
233124
  await service.unbind(req.params.actorId, req.auth.actor);
232774
233125
  return { status: 200, body: { ok: true } };
232775
233126
  });
@@ -232805,6 +233156,7 @@ function actorsDomain(opts) {
232805
233156
  });
232806
233157
  router.put("/api/actors/:id/connectors/:connectorId", async (req) => {
232807
233158
  const { service } = await resolveCtx(req.auth.companyId);
233159
+ await guardExistingActorWrite(req, service, req.params.id);
232808
233160
  const b2 = req.body;
232809
233161
  if (typeof b2?.enabled !== "boolean") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 enabled (boolean)");
232810
233162
  const conn = b2.enabled ? await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, true, req.auth.actor) : await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, false);
@@ -233973,6 +234325,7 @@ var init_routes3 = __esm({
233973
234325
  init_skill_materializer();
233974
234326
  init_connector_skills();
233975
234327
  init_runtime_tenancy();
234328
+ init_agent_manage_guard();
233976
234329
  CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
233977
234330
  RevealRateLimiter = class {
233978
234331
  constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
@@ -234304,7 +234657,7 @@ function createActorsDomain(opts) {
234304
234657
  return {
234305
234658
  service: defaultCtx.service,
234306
234659
  resolveCtx,
234307
- register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
234660
+ register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {}, ...opts.agentManageGuard ? { agentManageGuard: opts.agentManageGuard } : {} })
234308
234661
  };
234309
234662
  }
234310
234663
  var import_node_crypto55;
@@ -241577,11 +241930,34 @@ var init_service6 = __esm({
241577
241930
  async isEmployedElsewhere(companyId, accountId) {
241578
241931
  return isEmployedElsewhere(this.store, companyId, accountId);
241579
241932
  }
241580
- /** 当前用户所属的公司(按 membership 聚合)。 */
241933
+ /**
241934
+ * 当前用户所属的公司(按 membership 聚合)。**只返回 `status='active'` 的**。
241935
+ *
241936
+ * 🚨 过滤 status 是「解散组织」能真正生效的一半(组织三层身份 ADR D9.2):解散取的是可审计的
241937
+ * 停用语义(`status='suspended'` + `dissolved_at/by`),成员行**保留**。不在这里过滤的话,
241938
+ * 被解散的组织仍然会出现在顶栏切换器上、`CompanyGate` 也仍把这个人算作「有组织」——
241939
+ * 解散就只是换了个状态字,人照进不误。口径与 `resolveCurrentCompany` 的无 selector 分支对齐
241940
+ * (那一支本来就只挑 active)。某人全部组织都被解散后,按既有逻辑落到建团队页。
241941
+ */
241581
241942
  async listForUser(accountId) {
241582
241943
  const memberships = await this.store.listMembershipsForAccount(accountId);
241583
241944
  const fetched = await Promise.all(memberships.map((m2) => this.store.getCompany(m2.companyId)));
241584
- return fetched.filter((c) => c !== null && c !== void 0);
241945
+ return fetched.filter((c) => c !== null && c !== void 0 && c.status === "active");
241946
+ }
241947
+ /**
241948
+ * 「我在哪些组织里还是组织管理员」——注销账号的跨组织检查(ADR D9.3)。
241949
+ * 只看 `status='active'` 的组织:已解散的那家里是不是 owner 已经无关紧要。
241950
+ * **不能只看当前组织**:注销是账号级动作,漏一家就等于把那家组织甩成无人可管。
241951
+ */
241952
+ async listOwnedActiveCompanies(accountId) {
241953
+ const memberships = await this.store.listMembershipsForAccount(accountId);
241954
+ const out = [];
241955
+ for (const m2 of memberships) {
241956
+ if (m2.role !== "owner") continue;
241957
+ const c = await this.store.getCompany(m2.companyId);
241958
+ if (c?.status === "active") out.push(c);
241959
+ }
241960
+ return out;
241585
241961
  }
241586
241962
  async get(id) {
241587
241963
  return this.store.getCompany(id);
@@ -241646,11 +242022,49 @@ var init_service6 = __esm({
241646
242022
  return account;
241647
242023
  }
241648
242024
  /**
241649
- * 授予某邮箱在某公司的 owner(ADR 0009 切片5:bootstrap 种子 + grant-owner 兜底)。
241650
- * 确保账号存在 + upsert owner 成员。幂等(重复调把其角色置 owner)。
242025
+ * 授予某邮箱在某公司的组织管理员(ADR 0009 切片5:bootstrap 种子 + `grant-owner` 兜底)。
242026
+ *
242027
+ * 🚨 **它必须自己守 I1**(组织三层身份 ADR D2.4)。老实现是**无条件** upsert `role='owner'`,
242028
+ * 而调用点之一是 serve 的 bootstrap(`OASIS_BOOTSTRAP_OWNER`,在**启动路径**上)——该组织若已有
242029
+ * 组织管理员,它会写出第二位,在部分唯一索引 `company_members_one_owner_idx` 下直接抛错,
242030
+ * **等于 serve 起不来**。所以三分支:
242031
+ * - 目标已是该组织 owner → no-op,返回既有那行;
242032
+ * - 组织已有**别的** owner → **不写 owner**、`console.warn`,返回 `{ granted: false, existingOwner }`,
242033
+ * CLI 侧据此非零退出。但**仍然保证他是成员**:还不是成员就落一条 `admin`(已是成员的角色一个字不动)。
242034
+ * 为什么不是干脆「什么都不写」:这条通道的本意是 ADR 0009 的「第一个 owner 不会把自己锁在门外」
242035
+ * ——`OASIS_BOOTSTRAP_OWNER_EMAIL` 要让创始人**能登录并进得去管理面**。什么都不写的话,
242036
+ * 首装时若默认公司迁移已经指派过一位 owner,创始人就只有账号、没有任何 membership,
242037
+ * 登录后被 `CompanyGate` 扔到建团队页——兜底通道当场失效。管理面的闸是 `requireOwnerOrAdmin`,
242038
+ * admin 一样进得去;而「谁是唯一持有人」不该被一次装机悄悄换人。
242039
+ * - 组织无 owner(首次 bootstrap、或存量 0-owner 的脏数据)→ 照旧指派为 owner。
242040
+ * 「换人」一律走转交(`POST /api/companies/:id/owner`),不从这条兜底通道走。
241651
242041
  */
241652
242042
  async grantOwner(companyId, email2, name) {
241653
242043
  const account = await this.ensureAccount(email2, name);
242044
+ const members = await this.store.listMembers(companyId);
242045
+ const currentOwner = members.find((m2) => m2.role === "owner");
242046
+ if (currentOwner && currentOwner.accountId !== account.id) {
242047
+ const existingSelf = members.find((m2) => m2.accountId === account.id);
242048
+ if (existingSelf) {
242049
+ console.warn(
242050
+ `[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF08\u4ED6\u5DF2\u662F\u672C\u7EC4\u7EC7 ${existingSelf.role}\uFF0C\u89D2\u8272\u672A\u6539\uFF09\uFF1B\u5982\u9700\u66F4\u6362\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
242051
+ );
242052
+ return { granted: false, membership: existingSelf, existingOwner: currentOwner };
242053
+ }
242054
+ const asAdmin = {
242055
+ companyId,
242056
+ accountId: account.id,
242057
+ actorId: account.id,
242058
+ role: "admin",
242059
+ onboardingCompletedAt: this.now()
242060
+ };
242061
+ await this.store.upsertMember(asAdmin);
242062
+ console.warn(
242063
+ `[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF1B\u5DF2\u6539\u4E3A\u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002\u5982\u9700\u66F4\u6362\u6301\u6709\u4EBA\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
242064
+ );
242065
+ return { granted: false, membership: asAdmin, existingOwner: currentOwner };
242066
+ }
242067
+ if (currentOwner) return { granted: false, membership: currentOwner };
241654
242068
  const membership = {
241655
242069
  companyId,
241656
242070
  accountId: account.id,
@@ -241659,7 +242073,7 @@ var init_service6 = __esm({
241659
242073
  onboardingCompletedAt: this.now()
241660
242074
  };
241661
242075
  await this.store.upsertMember(membership);
241662
- return membership;
242076
+ return { granted: true, membership };
241663
242077
  }
241664
242078
  /**
241665
242079
  * 当前公司解析(动态、随登录用户走):
@@ -241672,6 +242086,7 @@ var init_service6 = __esm({
241672
242086
  let company;
241673
242087
  if (selector) {
241674
242088
  company = await this.store.getCompany(selector) ?? await this.store.getCompanyBySlug(selector);
242089
+ if (company && company.status !== "active") company = null;
241675
242090
  } else {
241676
242091
  company = null;
241677
242092
  for (const m2 of await this.store.listMembershipsForAccount(accountId)) {
@@ -241774,11 +242189,91 @@ var init_service6 = __esm({
241774
242189
  */
241775
242190
  async changeMemberRole(companyId, accountId, role) {
241776
242191
  if (!ROLES.includes(role)) throw new CompanyError(400, "BAD_ROLE", `role \u987B\u4E3A ${ROLES.join(" | ")}`);
242192
+ if (role === "owner") {
242193
+ throw new CompanyError(400, "OWNER_NOT_ASSIGNABLE", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EA\u80FD\u7ECF\u8F6C\u4EA4\u4EA7\u751F\uFF0C\u4E0D\u80FD\u76F4\u63A5\u6307\u6D3E");
242194
+ }
241777
242195
  await this.requireCompany(companyId);
242196
+ const target = await this.store.getMember(companyId, accountId);
242197
+ if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242198
+ if (target.role === "owner") throw this.ownerProtected();
241778
242199
  const res = await this.store.updateMemberRoleIfNotLastOwner(companyId, accountId, role);
241779
242200
  if (res.ok) return res.member;
241780
242201
  if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
241781
- throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u964D\u7EA7\u6700\u540E\u4E00\u4E2A owner");
242202
+ throw this.ownerProtected();
242203
+ }
242204
+ /** 「组织管理员受保护」的唯一错误来源——成员管理面的四个入口共用,码与文案不各写一份。 */
242205
+ ownerProtected() {
242206
+ return new CompanyError(403, "OWNER_PROTECTED", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u7F16\u8F91\u3001\u964D\u7EA7\u6216\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7");
242207
+ }
242208
+ /**
242209
+ * 转交组织管理员(`POST /api/companies/:id/owner`,ADR D4)。
242210
+ *
242211
+ * 调用者身份从会话解析、**不接受 body 里指定 from**。单事务、一把锁、**先降后升**,
242212
+ * 全部由 `store.setSoleCompanyOwner` 承担(并发语义见它的契约注释)。
242213
+ *
242214
+ * **刻意不做「已达成即 200」的幂等吞掉**:同一转交被重复提交时第二次同样得 409 `NOT_OWNER`。
242215
+ * 吞掉的话「我不是 owner 了」和「我刚转交成功」在响应里长得一样,前端分不出该显示
242216
+ * 「转交成功」还是「你的身份已变更」。
242217
+ *
242218
+ * **不 bump `tokenVersion`**:角色每请求现查(D7),转交不需要作废会话;bump 会把人从
242219
+ * **其它组织**一起踢下线(G10)。
242220
+ */
242221
+ async transferOwner(companyId, fromAccountId, toAccountId) {
242222
+ if (!toAccountId) throw new CompanyError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId");
242223
+ if (fromAccountId === toAccountId) {
242224
+ throw new CompanyError(400, "SELF_TRANSFER", "\u4E0D\u80FD\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u81EA\u5DF1");
242225
+ }
242226
+ await this.requireActiveCompany(companyId);
242227
+ const res = await this.store.setSoleCompanyOwner(companyId, toAccountId, { requireCurrentOwner: fromAccountId });
242228
+ if (!res.ok) {
242229
+ if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
242230
+ if (res.reason === "target_not_listed") {
242231
+ throw new CompanyError(409, "TARGET_NOT_LISTED", "\u5BF9\u65B9\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF08\u65B0\u624B\u6307\u5F15\u672A\u8D70\u5B8C\uFF09\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
242232
+ }
242233
+ if (res.reason === "target_inactive") {
242234
+ throw new CompanyError(409, "TARGET_INACTIVE", "\u5BF9\u65B9\u8D26\u53F7\u5DF2\u505C\u7528\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
242235
+ }
242236
+ throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
242237
+ }
242238
+ const from = res.demoted.find((m2) => m2.accountId === fromAccountId);
242239
+ if (!from) throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
242240
+ return { from, to: res.owner };
242241
+ }
242242
+ /**
242243
+ * 存量映射用的「把这家组织收敛成唯一组织管理员」(ADR D3 回填面)。
242244
+ * 与转交**同一把锁、同一顺序**(先降后升)——两份实现必然漂,而漂出来的差异正好落在
242245
+ * 「中途有没有出现两位持有人」这种只有并发下才现形的地方。
242246
+ */
242247
+ async setSoleOwner(companyId, toAccountId) {
242248
+ const res = await this.store.setSoleCompanyOwner(companyId, toAccountId);
242249
+ if (!res.ok) {
242250
+ if (res.reason === "target_not_listed") {
242251
+ throw new CompanyError(409, "TARGET_NOT_LISTED", `\u76EE\u6807\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF1A${toAccountId}`);
242252
+ }
242253
+ if (res.reason === "target_inactive") {
242254
+ throw new CompanyError(409, "TARGET_INACTIVE", `\u76EE\u6807\u8D26\u53F7\u5DF2\u505C\u7528\uFF1A${toAccountId}`);
242255
+ }
242256
+ throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
242257
+ }
242258
+ return { owner: res.owner, demoted: res.demoted };
242259
+ }
242260
+ /**
242261
+ * 解散组织(`DELETE /api/companies/:id`,ADR D9.2)——**可审计的停用,不是物理销毁**。
242262
+ * 仅当前组织管理员;「我还是不是 owner」在**同一把锁内**复核(D7.2),不是入口查一次就写。
242263
+ * 断访问靠 `resolveCurrentCompany` 只认 active(D8.4)+ `listForUser` 只列 active(D9.2)。
242264
+ */
242265
+ async dissolveCompany(companyId, callerAccountId) {
242266
+ await this.requireActiveCompany(companyId);
242267
+ const res = await this.store.dissolveCompanyIfOwner(companyId, callerAccountId, this.now());
242268
+ if (res.ok) return res.company;
242269
+ if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
242270
+ throw new CompanyError(403, "FORBIDDEN", "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EF\u4EE5\u89E3\u6563\u7EC4\u7EC7");
242271
+ }
242272
+ /** 公司必须存在**且 active**:已解散 / 已停用的组织上不接受任何写。 */
242273
+ async requireActiveCompany(companyId) {
242274
+ const c = await this.requireCompany(companyId);
242275
+ if (c.status !== "active") throw new CompanyError(404, "COMPANY_NOT_FOUND", `\u516C\u53F8\u4E0D\u53EF\u7528\uFF1A${companyId}`);
242276
+ return c;
241782
242277
  }
241783
242278
  /**
241784
242279
  * 改成员在本公司的昵称(组织模型 ADR §4.1 `company_member.display_name`)。
@@ -241805,9 +242300,7 @@ var init_service6 = __esm({
241805
242300
  await this.requireCompany(companyId);
241806
242301
  const member = await this.store.getMember(companyId, accountId);
241807
242302
  if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
241808
- if (member.role === "owner" && await this.isLastOwner(companyId, accountId)) {
241809
- throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
241810
- }
242303
+ if (member.role === "owner") throw this.ownerProtected();
241811
242304
  return member;
241812
242305
  }
241813
242306
  /**
@@ -241825,10 +242318,82 @@ var init_service6 = __esm({
241825
242318
  */
241826
242319
  async removeMember(companyId, accountId) {
241827
242320
  await this.requireCompany(companyId);
242321
+ const target = await this.store.getMember(companyId, accountId);
242322
+ if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242323
+ if (target.role === "owner") throw this.ownerProtected();
241828
242324
  const res = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
241829
242325
  if (res.ok) return res.member;
241830
242326
  if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
241831
- throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
242327
+ throw this.ownerProtected();
242328
+ }
242329
+ /**
242330
+ * 退出组织(`DELETE /api/companies/:id/members/me`,ADR D9.1)——**本人限定**。
242331
+ *
242332
+ * 组织管理员退不出:先转交、或(只剩他一个人时)解散。响应体带 `canDissolve`,
242333
+ * 前端据它把人引到「解散组织」而不是「先转交给谁」(brief:单人团队路径)。
242334
+ * 真正的移除副作用由 `MemberOffboardingService.applyMemberRemoval` 承担——与管理员移除他人
242335
+ * **同一套**,不在这里另写一份。
242336
+ */
242337
+ async peekSelfLeave(companyId, accountId) {
242338
+ await this.requireActiveCompany(companyId);
242339
+ const me = await this.store.getMember(companyId, accountId);
242340
+ if (!me) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242341
+ if (me.role === "owner") {
242342
+ const canDissolve = (await this.store.listMembers(companyId)).length <= 1;
242343
+ throw new CompanyError(
242344
+ 409,
242345
+ "OWNER_MUST_TRANSFER",
242346
+ canDissolve ? "\u4F60\u662F\u8FD9\u4E2A\u7EC4\u7EC7\u552F\u4E00\u7684\u6210\u5458\uFF0C\u9000\u51FA\u8BF7\u6539\u4E3A\u89E3\u6563\u7EC4\u7EC7" : "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u80FD\u9000\u51FA\u7EC4\u7EC7\uFF0C\u8BF7\u5148\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u4ED6\u4EBA",
242347
+ { canDissolve }
242348
+ );
242349
+ }
242350
+ return me;
242351
+ }
242352
+ /**
242353
+ * 注销账号的**跨组织前置检查**(`DELETE /api/accounts/me`,ADR D9.3)。
242354
+ *
242355
+ * 遍历该账号**全部** membership,取其中 `status='active'` 的组织:只要**任意一家**里还是
242356
+ * 组织管理员就拒(409 `OWNER_IN_ORGS`,响应体列出这些组织的 `{id, name}`),要求先转交或解散。
242357
+ * 🚨 这条检查天然跨组织、与「当前公司」无关——**不能**只看当前组织,漏一家就等于把那家组织
242358
+ * 甩成无人可管,而那个人已经注销、再也转交不了。
242359
+ *
242360
+ * 返回值 = 要逐家执行退出副作用的 membership 清单。**只含 active 组织**:已解散组织里的成员行
242361
+ * 原样保留(账号停用后进不去,解散组织本身也解析不出来),不为了「删干净」去碰一家已经归档的
242362
+ * 组织的成员名单——那会让它的历史成员名册凭空少人。
242363
+ */
242364
+ async peekAccountDeletion(accountId) {
242365
+ const account = await this.store.getAccount(accountId);
242366
+ if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
242367
+ const owned = await this.listOwnedActiveCompanies(accountId);
242368
+ if (owned.length > 0) {
242369
+ throw new CompanyError(
242370
+ 409,
242371
+ "OWNER_IN_ORGS",
242372
+ "\u4F60\u8FD8\u662F\u4EE5\u4E0B\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8BF7\u5148\u8F6C\u4EA4\u6216\u89E3\u6563\u540E\u518D\u6CE8\u9500",
242373
+ { companies: owned.map((c) => ({ id: c.id, name: c.name })) }
242374
+ );
242375
+ }
242376
+ const memberships = await this.store.listMembershipsForAccount(accountId);
242377
+ const out = [];
242378
+ for (const m2 of memberships) {
242379
+ const c = await this.store.getCompany(m2.companyId);
242380
+ if (c?.status === "active") out.push(m2);
242381
+ }
242382
+ return out;
242383
+ }
242384
+ /**
242385
+ * 注销的最后一步:账号置 `disabled` + **bump `tokenVersion`**,当场作废该账号全部会话。
242386
+ * 这是 `tokenVersion` 的**正确**用法(账号级总闸);转交与降级**不得**用它——那会把人从
242387
+ * 其它组织一起踢下线(G10)。
242388
+ *
242389
+ * **数据口径要诚实**:oplog 里的历史署名、已交付产物、任务轨迹**不删**(内核纯净红线)。
242390
+ * 「注销」= 这个人再也登不进来、不再是任何 active 组织的成员,不等于「个人数据被永久删除」。
242391
+ */
242392
+ async finalizeAccountDeletion(accountId) {
242393
+ const account = await this.store.getAccount(accountId);
242394
+ if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
242395
+ await this.store.upsertAccount({ ...account, status: "disabled" });
242396
+ await this.store.bumpAccountTokenVersion(accountId);
241832
242397
  }
241833
242398
  // —— 邀请(本地模型,CO-005)——
241834
242399
  /**
@@ -242263,21 +242828,18 @@ var init_service6 = __esm({
242263
242828
  if (!c) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
242264
242829
  return c;
242265
242830
  }
242266
- async isLastOwner(companyId, accountId) {
242267
- const owners = (await this.store.listMembers(companyId)).filter((m2) => m2.role === "owner");
242268
- return owners.length === 1 && owners[0].accountId === accountId;
242269
- }
242270
242831
  };
242271
242832
  }
242272
242833
  });
242273
242834
 
242274
242835
  // ../server/src/domains/companies/offboarding.ts
242275
- var import_node_crypto59, MemberOffboardingService;
242836
+ var import_node_crypto59, OWNER_PROTECTED_MESSAGE, MemberOffboardingService;
242276
242837
  var init_offboarding = __esm({
242277
242838
  "../server/src/domains/companies/offboarding.ts"() {
242278
242839
  "use strict";
242279
242840
  import_node_crypto59 = require("node:crypto");
242280
242841
  init_service6();
242842
+ OWNER_PROTECTED_MESSAGE = "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7";
242281
242843
  MemberOffboardingService = class {
242282
242844
  store;
242283
242845
  now;
@@ -242319,7 +242881,9 @@ var init_offboarding = __esm({
242319
242881
  if (!member) {
242320
242882
  return [{ code: "NOT_FOUND", message: `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}` }];
242321
242883
  }
242322
- if (await this.isLastOwner(companyId, accountId)) {
242884
+ if (member.role === "owner") {
242885
+ blockers.push({ code: "OWNER_PROTECTED", message: OWNER_PROTECTED_MESSAGE });
242886
+ } else if (await this.isLastOwner(companyId, accountId)) {
242323
242887
  blockers.push({ code: "LAST_OWNER", message: "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner" });
242324
242888
  }
242325
242889
  return blockers;
@@ -242342,6 +242906,7 @@ var init_offboarding = __esm({
242342
242906
  async plan(companyId, accountId, initiatedBy) {
242343
242907
  const member = await this.store.getMember(companyId, accountId);
242344
242908
  if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242909
+ if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
242345
242910
  if (await this.isLastOwner(companyId, accountId)) {
242346
242911
  throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
242347
242912
  }
@@ -242388,6 +242953,8 @@ var init_offboarding = __esm({
242388
242953
  if (op.version !== expectedVersion) {
242389
242954
  throw new CompanyError(409, "STALE_PLAN", `\u8BA1\u5212\u7248\u672C\u5DF2\u53D8\uFF08\u5F53\u524D ${op.version}\uFF0C\u4F60\u7ED9\u7684\u662F ${expectedVersion}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D6\u8BA1\u5212`);
242390
242955
  }
242956
+ const target = await this.store.getMember(companyId, accountId);
242957
+ if (target?.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
242391
242958
  if (await this.isLastOwner(companyId, accountId)) {
242392
242959
  throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
242393
242960
  }
@@ -242403,19 +242970,7 @@ var init_offboarding = __esm({
242403
242970
  );
242404
242971
  throw new CompanyError(409, remaining[0].code, `\u4ECD\u6709\u672A\u4EA4\u63A5\u9879\uFF1A${remaining.map((b2) => b2.message).join("; ")}`);
242405
242972
  }
242406
- const member = await this.store.getMember(companyId, accountId);
242407
- if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
242408
- await this.disableActor?.(companyId, member.actorId);
242409
- }
242410
- await this.store.deletePrincipalBridge(companyId, "human", accountId);
242411
- const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
242412
- if (!removed.ok) {
242413
- throw new CompanyError(
242414
- removed.reason === "last_owner" ? 409 : 404,
242415
- removed.reason === "last_owner" ? "LAST_OWNER" : "NOT_FOUND",
242416
- removed.reason === "last_owner" ? "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner" : `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`
242417
- );
242418
- }
242973
+ await this.applyMemberRemoval(companyId, accountId);
242419
242974
  await this.store.updateMemberOffboardingIfVersion(
242420
242975
  companyId,
242421
242976
  operationId,
@@ -242425,6 +242980,37 @@ var init_offboarding = __esm({
242425
242980
  );
242426
242981
  return { status: "removed", transferred: [], left: released };
242427
242982
  }
242983
+ /**
242984
+ * 移除一条成员关系的**全套副作用**,顺序固定,三条移除路径共用:
242985
+ * ① 清名下 agent 的负责人字段(`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,
242986
+ * 不清就是一个裸 FK 违例);**只清 owner**——不改状态、不撤凭证、不解绑助理(PRD v1.5);
242987
+ * ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行);
242988
+ * ③ 删 human principal bridge(对 company_member 是 CASCADE,但文件/内存实现没有级联,
242989
+ * 显式删一次让「桥随主体走」在应用层也成立);
242990
+ * ④ 删成员行(走 `removeMemberIfNotLastOwner`,组织管理员那一位在这里被纵深防御挡住)。
242991
+ *
242992
+ * 顺序不能反:成员行一删,本公司的 scoped registry 就不再认这个 actor,②会静默 no-op。
242993
+ * 任何一步失败都停在「人已停用、成员还在」——可重试,每一步都幂等。
242994
+ *
242995
+ * 三个调用点:管理员移除他人(`commit`)、本人退出组织(`DELETE …/members/me`)、
242996
+ * 注销账号(逐家执行,`DELETE /api/accounts/me`)。
242997
+ */
242998
+ async applyMemberRemoval(companyId, accountId) {
242999
+ const released = await this.releaseOwnedAgents(companyId, accountId);
243000
+ const member = await this.store.getMember(companyId, accountId);
243001
+ if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
243002
+ await this.disableActor?.(companyId, member.actorId);
243003
+ }
243004
+ await this.store.deletePrincipalBridge(companyId, "human", accountId);
243005
+ const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
243006
+ if (!removed.ok) {
243007
+ if (removed.reason === "not_found") {
243008
+ throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243009
+ }
243010
+ throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
243011
+ }
243012
+ return released;
243013
+ }
242428
243014
  /**
242429
243015
  * 把该成员名下所有 agent 的负责人字段清空,返回被清的 agent id。
242430
243016
  *
@@ -242519,6 +243105,12 @@ function companiesDomain(opts) {
242519
243105
  throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
242520
243106
  }
242521
243107
  };
243108
+ const requireMember = async (req, companyId) => {
243109
+ const me = await whoami(req);
243110
+ const member = await service.getMember(companyId, me.accountId);
243111
+ if (!member) throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
243112
+ return me;
243113
+ };
242522
243114
  return (router) => {
242523
243115
  router.get("/api/companies", async (req) => {
242524
243116
  const me = await whoami(req);
@@ -242578,7 +243170,9 @@ function companiesDomain(opts) {
242578
243170
  return { status: 200, body: { ok: true } };
242579
243171
  });
242580
243172
  router.get("/api/companies/:id/members", async (req) => {
242581
- const items = await service.listMemberViews(req.params.id).catch(mapErr);
243173
+ const cid = req.params.id;
243174
+ await requireMember(req, cid);
243175
+ const items = await service.listMemberViews(cid).catch(mapErr);
242582
243176
  return { status: 200, body: { items } };
242583
243177
  });
242584
243178
  router.get("/api/companies/:id/members/me", async (req) => {
@@ -242608,6 +243202,9 @@ function companiesDomain(opts) {
242608
243202
  if (b2.role === void 0 && b2.displayName === void 0) {
242609
243203
  throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08role / displayName\uFF09");
242610
243204
  }
243205
+ const me = await whoami(req);
243206
+ if (b2.role !== void 0 || me.accountId !== aid) await requireOwnerOrAdmin(req, cid);
243207
+ else await requireMember(req, cid);
242611
243208
  let member;
242612
243209
  if (b2.role !== void 0) {
242613
243210
  if (!b2.role) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 role");
@@ -242622,9 +243219,18 @@ function companiesDomain(opts) {
242622
243219
  }
242623
243220
  return { status: 200, body: member };
242624
243221
  });
243222
+ router.delete("/api/companies/:id/members/me", async (req) => {
243223
+ const off = requireOffboarding();
243224
+ const cid = req.params.id;
243225
+ const me = await whoami(req);
243226
+ await service.peekSelfLeave(cid, me.accountId).catch(mapErr);
243227
+ const released = await off.applyMemberRemoval(cid, me.accountId).catch(mapErr);
243228
+ return { status: 200, body: { ok: true, releasedAgents: released } };
243229
+ });
242625
243230
  router.delete("/api/companies/:id/members/:accountId", async (req) => {
242626
243231
  const cid = req.params.id;
242627
243232
  const aid = req.params.accountId;
243233
+ await requireOwnerOrAdmin(req, cid);
242628
243234
  const member = await service.peekRemovableMember(cid, aid).catch(mapErr);
242629
243235
  if (offboarding) await offboarding.releaseOwnedAgents(cid, aid).catch(mapErr);
242630
243236
  let actorsSvc;
@@ -242666,6 +243272,32 @@ function companiesDomain(opts) {
242666
243272
  }
242667
243273
  return { status: 200, body: { ok: true } };
242668
243274
  });
243275
+ router.post("/api/companies/:id/owner", async (req) => {
243276
+ const cid = req.params.id;
243277
+ const b2 = req.body ?? {};
243278
+ if (typeof b2.toAccountId !== "string" || !b2.toAccountId.trim()) {
243279
+ throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId: string");
243280
+ }
243281
+ const me = await whoami(req);
243282
+ const res = await service.transferOwner(cid, me.accountId, b2.toAccountId.trim()).catch(mapErr);
243283
+ return { status: 200, body: res };
243284
+ });
243285
+ router.delete("/api/companies/:id", async (req) => {
243286
+ const cid = req.params.id;
243287
+ const me = await whoami(req);
243288
+ const company = await service.dissolveCompany(cid, me.accountId).catch(mapErr);
243289
+ return { status: 200, body: company };
243290
+ });
243291
+ router.delete("/api/accounts/me", async (req) => {
243292
+ const off = requireOffboarding();
243293
+ const me = await whoami(req);
243294
+ const memberships = await service.peekAccountDeletion(me.accountId).catch(mapErr);
243295
+ for (const m2 of memberships) {
243296
+ await off.applyMemberRemoval(m2.companyId, me.accountId).catch(mapErr);
243297
+ }
243298
+ await service.finalizeAccountDeletion(me.accountId).catch(mapErr);
243299
+ return { status: 200, body: { ok: true, leftCompanies: memberships.map((m2) => m2.companyId) } };
243300
+ });
242669
243301
  router.get("/api/companies/:id/agent-credentials", async (req) => {
242670
243302
  const creds = requireCredentials();
242671
243303
  const cid = req.params.id;
@@ -242713,6 +243345,7 @@ function companiesDomain(opts) {
242713
243345
  router.post("/api/companies/:id/invitations", async (req) => {
242714
243346
  const b2 = req.body ?? {};
242715
243347
  if (!b2.email) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 email");
243348
+ await requireOwnerOrAdmin(req, req.params.id);
242716
243349
  const role = b2.role ?? "member";
242717
243350
  const me = await whoami(req);
242718
243351
  const invitation = await service.createInvitation(req.params.id, b2.email, role, me.accountId).catch(mapErr);
@@ -242852,7 +243485,7 @@ var init_routes6 = __esm({
242852
243485
  init_router();
242853
243486
  init_service6();
242854
243487
  init_image_upload();
242855
- COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links"];
243488
+ COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links", "/api/accounts"];
242856
243489
  }
242857
243490
  });
242858
243491
 
@@ -259991,7 +260624,35 @@ var init_pool = __esm({
259991
260624
  });
259992
260625
 
259993
260626
  // ../storage/src/postgres-control-plane.ts
259994
- var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
260627
+ async function ensureOneOwnerInvariant(pool, s2) {
260628
+ try {
260629
+ const bad = await pool.query(
260630
+ `SELECT company_id, count(*) FILTER (WHERE role = 'owner') AS owners
260631
+ FROM "${s2}".company_members
260632
+ GROUP BY company_id
260633
+ HAVING count(*) FILTER (WHERE role = 'owner') <> 1
260634
+ ORDER BY company_id`
260635
+ );
260636
+ if (bad.rowCount && bad.rowCount > 0) {
260637
+ console.warn(
260638
+ `[control-plane] ${bad.rowCount} \u5BB6\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u4E0D\u662F 1\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u4E0D\u81EA\u52A8\u6311\u4EBA\uFF09\uFF1A` + bad.rows.map((r) => `${r.company_id}=${r.owners}`).join(", ") + `\u3002\u7528 \`pnpm migrate:org-identity inventory\` \u51FA\u76D8\u70B9\u3001\u6309\u51BB\u7ED3 manifest \u8DD1 backfill\u3002`
260639
+ );
260640
+ }
260641
+ } catch (e) {
260642
+ console.warn(`[control-plane] \u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u590D\u6838\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`);
260643
+ }
260644
+ try {
260645
+ await pool.query(
260646
+ `CREATE UNIQUE INDEX IF NOT EXISTS company_members_one_owner_idx
260647
+ ON "${s2}".company_members (company_id) WHERE role = 'owner'`
260648
+ );
260649
+ } catch (e) {
260650
+ console.warn(
260651
+ `[control-plane] \u5EFA company_members_one_owner_idx \u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF0C\u5E94\u7528\u5C42\u5224\u636E\u4ECD\u5728\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`
260652
+ );
260653
+ }
260654
+ }
260655
+ var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
259995
260656
  var init_postgres_control_plane = __esm({
259996
260657
  "../storage/src/postgres-control-plane.ts"() {
259997
260658
  "use strict";
@@ -260021,6 +260682,8 @@ var init_postgres_control_plane = __esm({
260021
260682
  onboarding_variant text -- \u767B\u5F55\u540E\u52A9\u7406\u5F15\u5BFC\u53D8\u4F53 A|B\uFF1BNULL=\u7F3A\u7701\u89C6\u4E3A A
260022
260683
  )`);
260023
260684
  await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS onboarding_variant text`);
260685
+ await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_at timestamptz`);
260686
+ await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_by text`);
260024
260687
  await pool.query(`
260025
260688
  CREATE TABLE IF NOT EXISTS "${s2}".company_members (
260026
260689
  company_id text NOT NULL,
@@ -260209,6 +260872,19 @@ var init_postgres_control_plane = __esm({
260209
260872
  CHECK (status IN ('handoff_required','handoff_in_progress','ready_to_remove','removed'))
260210
260873
  )`);
260211
260874
  await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS member_offboardings_active_uq ON "${s2}".member_offboardings (company_id, account_id) WHERE status <> 'removed'`);
260875
+ await pool.query(`
260876
+ CREATE TABLE IF NOT EXISTS "${s2}".company_member_role_migrations (
260877
+ id bigserial PRIMARY KEY,
260878
+ company_id text NOT NULL,
260879
+ account_id text NOT NULL,
260880
+ role_before text NOT NULL,
260881
+ role_after text NOT NULL,
260882
+ evidence text NOT NULL, -- 'E1' | 'manifest'
260883
+ manifest_version text,
260884
+ applied_at timestamptz NOT NULL DEFAULT now()
260885
+ )`);
260886
+ await pool.query(`CREATE INDEX IF NOT EXISTS company_member_role_migrations_company_idx ON "${s2}".company_member_role_migrations (company_id, applied_at DESC)`);
260887
+ await ensureOneOwnerInvariant(pool, s2);
260212
260888
  return new _PostgresControlPlaneStore(pool, schema);
260213
260889
  }
260214
260890
  // —— 公司 ——
@@ -260249,11 +260925,51 @@ var init_postgres_control_plane = __esm({
260249
260925
  args.push(patch.onboardingVariant);
260250
260926
  sets.push(`onboarding_variant = $${args.length}`);
260251
260927
  }
260928
+ if (patch.dissolvedAt !== void 0) {
260929
+ args.push(patch.dissolvedAt);
260930
+ sets.push(`dissolved_at = $${args.length}`);
260931
+ }
260932
+ if (patch.dissolvedBy !== void 0) {
260933
+ args.push(patch.dissolvedBy);
260934
+ sets.push(`dissolved_by = $${args.length}`);
260935
+ }
260252
260936
  args.push(id);
260253
260937
  const sql = sets.length ? `UPDATE ${this.s}.companies SET ${sets.join(", ")} WHERE id = $${args.length}` : `SELECT 1 FROM ${this.s}.companies WHERE id = $${args.length}`;
260254
260938
  const r = await this.pool.query(sql, args);
260255
260939
  if (r.rowCount === 0) throw new Error(`company not found: ${id}`);
260256
260940
  }
260941
+ async dissolveCompanyIfOwner(companyId, accountId, at2) {
260942
+ const client = await this.pool.connect();
260943
+ try {
260944
+ await client.query("BEGIN");
260945
+ const caller = await client.query(
260946
+ `SELECT role FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2 FOR UPDATE`,
260947
+ [companyId, accountId]
260948
+ );
260949
+ if (!caller.rows[0] || caller.rows[0].role !== "owner") {
260950
+ await client.query("ROLLBACK");
260951
+ const exists = await this.pool.query(`SELECT 1 FROM ${this.s}.companies WHERE id = $1`, [companyId]);
260952
+ return { ok: false, reason: exists.rowCount ? "not_owner" : "not_found" };
260953
+ }
260954
+ const updated = await client.query(
260955
+ `UPDATE ${this.s}.companies SET status = 'suspended', dissolved_at = $2, dissolved_by = $3
260956
+ WHERE id = $1 RETURNING *`,
260957
+ [companyId, at2, accountId]
260958
+ );
260959
+ if (!updated.rows[0]) {
260960
+ await client.query("ROLLBACK");
260961
+ return { ok: false, reason: "not_found" };
260962
+ }
260963
+ await client.query("COMMIT");
260964
+ return { ok: true, company: rowToCompany(updated.rows[0]) };
260965
+ } catch (e) {
260966
+ await client.query("ROLLBACK").catch(() => {
260967
+ });
260968
+ throw e;
260969
+ } finally {
260970
+ client.release();
260971
+ }
260972
+ }
260257
260973
  // —— 成员 ——
260258
260974
  async upsertMember(m2) {
260259
260975
  await this.pool.query(
@@ -260365,6 +261081,103 @@ var init_postgres_control_plane = __esm({
260365
261081
  client.release();
260366
261082
  }
260367
261083
  }
261084
+ async setSoleCompanyOwner(companyId, toAccountId, opts) {
261085
+ const client = await this.pool.connect();
261086
+ try {
261087
+ await client.query("BEGIN");
261088
+ const all2 = await client.query(
261089
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261090
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261091
+ [companyId]
261092
+ );
261093
+ const rows = all2.rows.map(rowToMember);
261094
+ const target = rows.find((m2) => m2.accountId === toAccountId);
261095
+ if (!target) {
261096
+ await client.query("ROLLBACK");
261097
+ return { ok: false, reason: "not_found" };
261098
+ }
261099
+ if (opts?.requireCurrentOwner !== void 0) {
261100
+ const caller = rows.find((m2) => m2.accountId === opts.requireCurrentOwner);
261101
+ if (!caller || caller.role !== "owner") {
261102
+ await client.query("ROLLBACK");
261103
+ return { ok: false, reason: "not_owner" };
261104
+ }
261105
+ }
261106
+ if (!target.onboardingCompletedAt && target.role !== "owner") {
261107
+ await client.query("ROLLBACK");
261108
+ return { ok: false, reason: "target_not_listed" };
261109
+ }
261110
+ const acc = await client.query(
261111
+ `SELECT status FROM ${this.s}.accounts WHERE id = $1`,
261112
+ [toAccountId]
261113
+ );
261114
+ if (acc.rows[0] && acc.rows[0].status !== "active") {
261115
+ await client.query("ROLLBACK");
261116
+ return { ok: false, reason: "target_inactive" };
261117
+ }
261118
+ const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
261119
+ if (others.length === 0 && target.role === "owner") {
261120
+ await client.query("COMMIT");
261121
+ return { ok: true, owner: target, demoted: [] };
261122
+ }
261123
+ if (others.length > 0) {
261124
+ await client.query(
261125
+ `UPDATE ${this.s}.company_members SET role = 'admin', updated_at = now()
261126
+ WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
261127
+ [companyId, toAccountId]
261128
+ );
261129
+ }
261130
+ if (target.role !== "owner") {
261131
+ await client.query(
261132
+ `UPDATE ${this.s}.company_members SET role = 'owner', updated_at = now()
261133
+ WHERE company_id = $1 AND account_id = $2`,
261134
+ [companyId, toAccountId]
261135
+ );
261136
+ }
261137
+ await client.query("COMMIT");
261138
+ return {
261139
+ ok: true,
261140
+ owner: { ...target, role: "owner" },
261141
+ demoted: others.map((m2) => ({ ...m2, role: "admin" }))
261142
+ };
261143
+ } catch (e) {
261144
+ await client.query("ROLLBACK").catch(() => {
261145
+ });
261146
+ throw e;
261147
+ } finally {
261148
+ client.release();
261149
+ }
261150
+ }
261151
+ // —— 存量身份映射审计(ADR D3.4)——
261152
+ async appendMemberRoleMigrations(rows) {
261153
+ if (rows.length === 0) return;
261154
+ const client = await this.pool.connect();
261155
+ try {
261156
+ await client.query("BEGIN");
261157
+ for (const r of rows) {
261158
+ await client.query(
261159
+ `INSERT INTO ${this.s}.company_member_role_migrations
261160
+ (company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
261161
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
261162
+ [r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
261163
+ );
261164
+ }
261165
+ await client.query("COMMIT");
261166
+ } catch (e) {
261167
+ await client.query("ROLLBACK").catch(() => {
261168
+ });
261169
+ throw e;
261170
+ } finally {
261171
+ client.release();
261172
+ }
261173
+ }
261174
+ async listMemberRoleMigrations(companyId) {
261175
+ const r = companyId === void 0 ? await this.pool.query(`SELECT * FROM ${this.s}.company_member_role_migrations ORDER BY applied_at, id`) : await this.pool.query(
261176
+ `SELECT * FROM ${this.s}.company_member_role_migrations WHERE company_id = $1 ORDER BY applied_at, id`,
261177
+ [companyId]
261178
+ );
261179
+ return r.rows.map(rowToRoleMigration);
261180
+ }
260368
261181
  // —— 邀请 ——
260369
261182
  async createInvitation(inv) {
260370
261183
  await this.pool.query(
@@ -260834,7 +261647,19 @@ var init_postgres_control_plane = __esm({
260834
261647
  ...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
260835
261648
  status: row.status,
260836
261649
  createdAt: new Date(row.created_at).toISOString(),
260837
- ...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {}
261650
+ ...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {},
261651
+ // 解散痕迹(组织三层身份 ADR D9.2):两列一起有、一起空。普通运维停用不写它们。
261652
+ ...row.dissolved_at != null ? { dissolvedAt: toIso(row.dissolved_at) } : {},
261653
+ ...row.dissolved_by != null ? { dissolvedBy: row.dissolved_by } : {}
261654
+ });
261655
+ rowToRoleMigration = (row) => ({
261656
+ companyId: row.company_id,
261657
+ accountId: row.account_id,
261658
+ roleBefore: row.role_before,
261659
+ roleAfter: row.role_after,
261660
+ evidence: row.evidence,
261661
+ ...row.manifest_version != null ? { manifestVersion: row.manifest_version } : {},
261662
+ appliedAt: toIso(row.applied_at)
260838
261663
  });
260839
261664
  rowToMember = (row) => ({
260840
261665
  companyId: row.company_id,
@@ -276559,6 +277384,7 @@ __export(src_exports2, {
276559
277384
  ACTOR_DELETED_CHAT_MESSAGE: () => ACTOR_DELETED_CHAT_MESSAGE,
276560
277385
  ACTOR_DELETED_ERROR_CODE: () => ACTOR_DELETED_ERROR_CODE,
276561
277386
  ASSISTANT_ARCHIVED_CODE: () => ASSISTANT_ARCHIVED_CODE,
277387
+ ASSISTANT_POSITION: () => ASSISTANT_POSITION,
276562
277388
  ASSISTANT_TIER_RULES: () => ASSISTANT_TIER_RULES,
276563
277389
  AUTOMATION_TEMPLATES: () => AUTOMATION_TEMPLATES,
276564
277390
  AUTONOMOUS_ETHOS: () => AUTONOMOUS_ETHOS,
@@ -276566,6 +277392,7 @@ __export(src_exports2, {
276566
277392
  ActorMemoryService: () => ActorMemoryService,
276567
277393
  ActorsService: () => ActorsService,
276568
277394
  AgentCredentialService: () => AgentCredentialService,
277395
+ AgentManageForbiddenError: () => AgentManageForbiddenError,
276569
277396
  ApiError: () => ApiError,
276570
277397
  AssistantLazyCreateError: () => AssistantLazyCreateError,
276571
277398
  AssistantsService: () => AssistantsService,
@@ -276703,6 +277530,7 @@ __export(src_exports2, {
276703
277530
  OPERATOR_SCOPE: () => OPERATOR_SCOPE,
276704
277531
  ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
276705
277532
  OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
277533
+ OWNER_PROTECTED_MESSAGE: () => OWNER_PROTECTED_MESSAGE,
276706
277534
  PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
276707
277535
  PARENT_CONVERSATION_RECENT_TURNS: () => PARENT_CONVERSATION_RECENT_TURNS,
276708
277536
  PostgresNotificationStore: () => PostgresNotificationStore,
@@ -276757,11 +277585,16 @@ __export(src_exports2, {
276757
277585
  applyWorkspaceBundle: () => applyWorkspaceBundle,
276758
277586
  assembleDelegationsDomain: () => assembleDelegationsDomain,
276759
277587
  assertBindingWritePermission: () => assertBindingWritePermission,
277588
+ assertCompanyManager: () => assertCompanyManager,
276760
277589
  assertCreatableVariable: () => assertCreatableVariable,
277590
+ assertMayCreateAgent: () => assertMayCreateAgent,
277591
+ assertMayManageAgent: () => assertMayManageAgent,
277592
+ assertMaySelfOrManage: () => assertMaySelfOrManage,
276761
277593
  assistantRowRecovery: () => assistantRowRecovery,
276762
277594
  assistantsDomain: () => assistantsDomain,
276763
277595
  atCapacity: () => atCapacity,
276764
277596
  automationsDomain: () => automationsDomain,
277597
+ backfillOrgIdentity: () => backfillOrgIdentity,
276765
277598
  backfillOrgModel: () => backfillOrgModel,
276766
277599
  backfillOrgModelOnStartup: () => backfillOrgModelOnStartup,
276767
277600
  backfillResolvedLetterReadMarkers: () => backfillResolvedLetterReadMarkers,
@@ -276934,8 +277767,11 @@ __export(src_exports2, {
276934
277767
  hideDeleted: () => hideDeleted,
276935
277768
  humanSubmitSourceRunId: () => humanSubmitSourceRunId,
276936
277769
  idlenessOf: () => idlenessOf,
277770
+ inventoryOrgIdentity: () => inventoryOrgIdentity,
276937
277771
  isActiveCoordinatorAgent: () => isActiveCoordinatorAgent,
277772
+ isAssistantAgent: () => isAssistantAgent,
276938
277773
  isCompanyCrossTenantPath: () => isCompanyCrossTenantPath,
277774
+ isCompanyManager: () => isCompanyManager,
276939
277775
  isCoordinatorAgentName: () => isCoordinatorAgentName,
276940
277776
  isCoordinatorRunReader: () => isCoordinatorRunReader,
276941
277777
  isCredentialAuditKind: () => isCredentialAuditKind,
@@ -276995,6 +277831,7 @@ __export(src_exports2, {
276995
277831
  matchedKnowledgeQueryTerms: () => matchedKnowledgeQueryTerms,
276996
277832
  materializeBuiltinSkills: () => materializeBuiltinSkills,
276997
277833
  materializeSkillFiles: () => materializeSkillFiles,
277834
+ mayManageAgent: () => mayManageAgent,
276998
277835
  mayManageWorkorder: () => mayManageWorkorder,
276999
277836
  mayReassignProjectItem: () => mayReassignProjectItem,
277000
277837
  memberNameResolver: () => memberNameResolver,
@@ -277109,6 +277946,7 @@ __export(src_exports2, {
277109
277946
  retireCdGateInTypeDef: () => retireCdGateInTypeDef,
277110
277947
  retireCdGateInTypeStore: () => retireCdGateInTypeStore,
277111
277948
  rolesDomain: () => rolesDomain,
277949
+ rollbackOrgIdentity: () => rollbackOrgIdentity,
277112
277950
  routeFusedSignal: () => routeFusedSignal,
277113
277951
  runtimeConnectionStatus: () => runtimeConnectionStatus,
277114
277952
  runtimeDriftBlock: () => runtimeDriftBlock,
@@ -277200,6 +278038,7 @@ var init_src11 = __esm({
277200
278038
  init_outputs_scan();
277201
278039
  init_outputs_copy();
277202
278040
  init_org_model_backfill();
278041
+ init_org_identity_migration();
277203
278042
  init_types_admin();
277204
278043
  init_roles_admin();
277205
278044
  init_roles_seed();
@@ -277311,6 +278150,7 @@ var init_src11 = __esm({
277311
278150
  init_chat_builtin_skills();
277312
278151
  init_connector_skills();
277313
278152
  init_connector_tool_versions();
278153
+ init_agent_manage_guard();
277314
278154
  init_board_watch();
277315
278155
  init_uncategorized_project();
277316
278156
  init_run_settlement();
@@ -280989,6 +281829,28 @@ async function startServe(opts) {
280989
281829
  const role = members.find((m2) => m2.actorId === actorId)?.role;
280990
281830
  return role === "owner" || role === "admin" || role === "member" ? role : null;
280991
281831
  },
281832
+ /* Agent 写面的行政权闸(组织三层身份 ADR D6,判据在 `governance/agent-manage-guard.ts`)。
281833
+ 三条读取面都**每请求现查**、都按 `(组织, 账号)` 双键——禁止把角色塞进任何跨请求缓存
281834
+ (D7.1:`agentPrincipalByToken` 只缓存 `{companyId, actorId}`,不许顺手加 role)。
281835
+ `controlPlaneStore` / `getEngine` / `resolveAssistantsCtx` 在下方才就绪;这三个箭头只在
281836
+ 请求时跑(同 `projectExists` 的惰性模式)。 */
281837
+ agentManageGuard: {
281838
+ callerRole: async (companyId, callerActorId) => {
281839
+ const cid = companyId ?? defaultCompanyId;
281840
+ const members = await controlPlaneStore.listMembers(cid);
281841
+ const row = members.find((m2) => m2.actorId === callerActorId || m2.accountId === callerActorId);
281842
+ return row?.role;
281843
+ },
281844
+ boundHuman: async (companyId, agentId) => {
281845
+ const ctx = await resolveAssistantsCtxLazy(companyId);
281846
+ if (!ctx) return null;
281847
+ return (await ctx.service.getByAgent(agentId))?.humanActorId ?? null;
281848
+ },
281849
+ reportsTo: async (companyId, humanActorId) => {
281850
+ const engine2 = await getEngine(companyId ?? defaultCompanyId);
281851
+ return (await engine2.registry.getActor(humanActorId))?.reportsTo ?? null;
281852
+ }
281853
+ },
280992
281854
  // CO-302 数据面隔离(actors 域试点):按当前公司取其引擎,用各自 registry 服务该域请求。
280993
281855
  // engineRouter 在下方(行 ~411)以 const 声明;此箭头只在请求时调用,那时已初始化,闭包引用合法
280994
281856
  // (TDZ 只在构造时访问才报错,这里不访问)。默认公司命中现有单实例(registry===registryStore),
@@ -281051,6 +281913,12 @@ async function startServe(opts) {
281051
281913
  isPlatformAdmin: async () => false
281052
281914
  // 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
281053
281915
  });
281916
+ let resolveAssistantsCtxImpl = null;
281917
+ const resolveAssistantsCtx = async (companyId) => {
281918
+ if (!resolveAssistantsCtxImpl) throw new Error("assistants ctx resolver \u5C1A\u672A\u88C5\u914D\uFF08\u542F\u52A8\u671F\u4E0D\u5E94\u6709\u8BF7\u6C42\uFF09");
281919
+ return resolveAssistantsCtxImpl(companyId);
281920
+ };
281921
+ const resolveAssistantsCtxLazy = async (companyId) => resolveAssistantsCtxImpl ? resolveAssistantsCtxImpl(companyId) : null;
281054
281922
  let projectStateStore;
281055
281923
  let artifactStateStore;
281056
281924
  let chatSessionStore;
@@ -281780,8 +282648,7 @@ async function startServe(opts) {
281780
282648
  const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, sharedSchema) : await FileControlPlaneStore.open(path30.join(opts.dir, "control-plane.json"));
281781
282649
  assistantsService.setPlatformAdminChecker(async (caller) => {
281782
282650
  const member = await controlPlaneStore.getMember(defaultCompanyId, caller);
281783
- if (!member) return false;
281784
- return member.role === "owner";
282651
+ return member?.role === "owner" || member?.role === "admin";
281785
282652
  });
281786
282653
  if (pgDsn && pgPool) {
281787
282654
  const priceStore = await PostgresModelPriceStore.open(pgPool, sharedSchema);
@@ -281879,16 +282746,29 @@ async function startServe(opts) {
281879
282746
  }
281880
282747
  }
281881
282748
  if (!await controlPlaneStore.getMember(defaultCompanyId, operatorActor)) {
281882
- await controlPlaneStore.upsertMember({ companyId: defaultCompanyId, accountId: operatorActor, actorId: operatorActor, role: "owner", onboardingCompletedAt: seededAt });
282749
+ const hasOwner = (await controlPlaneStore.listMembers(defaultCompanyId)).some((m2) => m2.role === "owner");
282750
+ await controlPlaneStore.upsertMember({
282751
+ companyId: defaultCompanyId,
282752
+ accountId: operatorActor,
282753
+ actorId: operatorActor,
282754
+ role: hasOwner ? "admin" : "owner",
282755
+ onboardingCompletedAt: seededAt
282756
+ });
282757
+ if (hasOwner) {
282758
+ console.log(`[serve] \u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8FD0\u7EF4\u8005 ${operatorActor} \u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09`);
282759
+ }
281883
282760
  }
281884
282761
  }
281885
282762
  const bootstrapEmail = process.env["OASIS_BOOTSTRAP_OWNER_EMAIL"];
281886
282763
  if (bootstrapEmail) {
281887
- const m2 = await companies.service.grantOwner(defaultCompanyId, bootstrapEmail);
281888
- console.log(`[serve] bootstrap owner\uFF1A${bootstrapEmail} \u2192 ${m2.accountId}\uFF08\u9ED8\u8BA4\u516C\u53F8 owner\uFF09`);
282764
+ const res = await companies.service.grantOwner(defaultCompanyId, bootstrapEmail);
282765
+ const bootstrapAccount = await controlPlaneStore.getAccountByEmail(bootstrapEmail.trim().toLowerCase());
282766
+ console.log(
282767
+ res.granted ? `[serve] bootstrap owner\uFF1A${bootstrapEmail} \u2192 ${res.membership.accountId}\uFF08\u9ED8\u8BA4\u516C\u53F8 owner\uFF09` : `[serve] bootstrap owner \u8DF3\u8FC7\uFF1A\u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.membership.accountId}\uFF08\u6362\u4EBA\u8D70\u8F6C\u4EA4\uFF09`
282768
+ );
281889
282769
  const bootstrapPw = process.env["OASIS_BOOTSTRAP_OWNER_PASSWORD"];
281890
- if (bootstrapPw) {
281891
- await controlPlaneStore.setAccountPassword(m2.accountId, hashPassword(bootstrapPw));
282770
+ if (bootstrapPw && bootstrapAccount) {
282771
+ await controlPlaneStore.setAccountPassword(bootstrapAccount.id, hashPassword(bootstrapPw));
281892
282772
  console.log(`[serve] bootstrap owner \u5BC6\u7801\u5DF2\u8BBE\uFF08OASIS_BOOTSTRAP_OWNER_PASSWORD\uFF09`);
281893
282773
  }
281894
282774
  }
@@ -282060,6 +282940,36 @@ async function startServe(opts) {
282060
282940
  planeInflight.set(companyId, building);
282061
282941
  return building;
282062
282942
  };
282943
+ resolveAssistantsCtxImpl = async (companyId) => {
282944
+ const cid = companyId ?? defaultCompanyId;
282945
+ const ctx = await actors.resolveCtx(cid);
282946
+ const engine2 = await getEngine(cid);
282947
+ const primary = Boolean(
282948
+ pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
282949
+ );
282950
+ const plane = primary ? await openCompanyPlane(cid) : void 0;
282951
+ return {
282952
+ service: new AssistantsService({
282953
+ bindStore: plane?.assistants ?? assistantBindStore,
282954
+ prefsStore: plane?.humanPrefs ?? humanPrefsStore,
282955
+ registry: engine2.registry,
282956
+ actors: ctx.service,
282957
+ // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
282958
+ companyId: () => cid,
282959
+ listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
282960
+ // owner | admin 同档(组织三层身份 ADR D6.4):只认 owner 会让「管理员」失去今天
282961
+ // 「组织管理者」本就有的助理管理能力——那是本次改动造成的能力回归,不是新规则。
282962
+ isPlatformAdmin: async (caller) => {
282963
+ const member = await controlPlaneStore.getMember(cid, caller);
282964
+ return member?.role === "owner" || member?.role === "admin";
282965
+ },
282966
+ // 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
282967
+ // (新模型下它恰好返回一个元素,`ownerAssistantRuntime` 的「取第一个有着落的」语义不变)。
282968
+ listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
282969
+ }),
282970
+ getActor: (id) => engine2.registry.getActor(id)
282971
+ };
282972
+ };
282063
282973
  const openCompanyPlaneUncached = async (companyId) => {
282064
282974
  if (!pgPool || !locationStore || !cutoverStore) {
282065
282975
  throw new Error("openCompanyPlane \u9700\u8981 Postgres");
@@ -283121,34 +284031,10 @@ async function startServe(opts) {
283121
284031
  assistantsDomain({
283122
284032
  service: assistantsService,
283123
284033
  getActor: (id) => registryStore.getActor(id),
283124
- resolveService: async (companyId) => {
283125
- const cid = companyId ?? defaultCompanyId;
283126
- const ctx = await actors.resolveCtx(cid);
283127
- const engine2 = await getEngine(cid);
283128
- const primary = Boolean(
283129
- pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
283130
- );
283131
- const plane = primary ? await openCompanyPlane(cid) : void 0;
283132
- return {
283133
- service: new AssistantsService({
283134
- bindStore: plane?.assistants ?? assistantBindStore,
283135
- prefsStore: plane?.humanPrefs ?? humanPrefsStore,
283136
- registry: engine2.registry,
283137
- actors: ctx.service,
283138
- // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
283139
- companyId: () => cid,
283140
- listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
283141
- isPlatformAdmin: async (caller) => {
283142
- const member = await controlPlaneStore.getMember(cid, caller);
283143
- return member?.role === "owner";
283144
- },
283145
- // 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
283146
- // (第一个 owner = 建团队的人;三种 store 都按加入先后返回,见控制面一致性套件)。
283147
- listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
283148
- }),
283149
- getActor: (id) => engine2.registry.getActor(id)
283150
- };
283151
- }
284034
+ // 与 actors 域的 agentManageGuard.boundHuman **同一份**:「这个 agent 绑给了哪个真人」
284035
+ // 两处各解析一次助理 store 必然漂,而漂出来的差异正好是「删助理」与「解绑助理」两条路
284036
+ // 的闸又对不上(G4 的形状)。
284037
+ resolveService: resolveAssistantsCtx
283152
284038
  }),
283153
284039
  companies.register,
283154
284040
  projectsDomain2.register,
@@ -292604,7 +293490,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
292604
293490
  }
292605
293491
  }
292606
293492
  }
292607
- var CLI_VERSION = "2.2.80" ? "2.2.80" : "dev";
293493
+ var CLI_VERSION = "2.2.81" ? "2.2.81" : "dev";
292608
293494
  function apiHeaders(token, company) {
292609
293495
  const headers = {
292610
293496
  authorization: `Bearer ${token}`,
@@ -293001,8 +293887,19 @@ async function runCli(argv, println = console.log, progressln = console.error) {
293001
293887
  const name = flags.get("name");
293002
293888
  if (!await store.getCompany(companyId)) throw new Error(`\u516C\u53F8\u4E0D\u5B58\u5728\uFF1A${companyId}\uFF08\u5148\u8BA9 serve \u8DD1\u4E00\u6B21\u4EE5\u5EFA\u9ED8\u8BA4\u516C\u53F8\uFF0C\u6216\u7528 --company \u6307\u5B9A\u5DF2\u5B58\u5728\u516C\u53F8\uFF09`);
293003
293889
  const svc = new CompaniesService({ store });
293004
- const m2 = await svc.grantOwner(companyId, email2, name);
293005
- println(`\u5DF2\u6388 owner\uFF1A${email2} \u2192 ${m2.accountId}\uFF08\u516C\u53F8 ${companyId}\uFF09`);
293890
+ const res = await svc.grantOwner(companyId, email2, name);
293891
+ if (!res.granted && res.existingOwner) {
293892
+ process.stderr.write(
293893
+ `\u516C\u53F8 ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.existingOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${email2}\uFF1B\u4ED6\u73B0\u5728\u662F\u672C\u7EC4\u7EC7\u7684\u300C${res.membership.role === "admin" ? "\u7BA1\u7406\u5458" : res.membership.role}\u300D\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002
293894
+ \u6362\u4EBA\u8BF7\u8BA9\u73B0\u4EFB\u6301\u6709\u4EBA\u8C03 POST /api/companies/${companyId}/owner\uFF08\u8F6C\u4EA4\uFF09\u3002
293895
+ `
293896
+ );
293897
+ process.exitCode = 1;
293898
+ return;
293899
+ }
293900
+ println(
293901
+ res.granted ? `\u5DF2\u6388 owner\uFF1A${email2} \u2192 ${res.membership.accountId}\uFF08\u516C\u53F8 ${companyId}\uFF09` : `${email2} \u5DF2\u662F\u516C\u53F8 ${companyId} \u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u65E0\u9700\u6539\u52A8\uFF08\u5E42\u7B49\uFF09`
293902
+ );
293006
293903
  process.stderr.write("\u63D0\u793A\uFF1A\u82E5 serve \u6B63\u5728\u8FD0\u884C\uFF0C\u63A7\u5236\u9762\u6587\u4EF6\u65E0\u9501\uFF0C\u5EFA\u8BAE\u505C\u6B62 serve \u540E\u518D\u6267\u884C\u4EE5\u514D\u5E76\u53D1\u5199\u3002\n");
293007
293904
  return;
293008
293905
  }
@@ -296503,7 +297400,7 @@ function shimScript() {
296503
297400
  }
296504
297401
 
296505
297402
  // src/index.ts
296506
- var PKG_VERSION = true ? "2.2.80" : "dev";
297403
+ var PKG_VERSION = true ? "2.2.81" : "dev";
296507
297404
  var LOCAL_BIN = localBin();
296508
297405
  var NPM_PREFIX = npmPrefix();
296509
297406
  var INSTANCE = DEFAULT_INSTANCE;