oasis_test_v2 2.2.80 → 2.2.82

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 +1241 -92
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2950,6 +2950,25 @@ async function fetchResultPackage(base, fetchImpl, runId) {
2950
2950
  }));
2951
2951
  return { artifacts, fileCount: body2.project?.fileCount ?? artifacts.length };
2952
2952
  }
2953
+ function toEpochMs(v2) {
2954
+ if (typeof v2 === "number" && Number.isFinite(v2)) return v2;
2955
+ if (typeof v2 === "string" && v2.length > 0) {
2956
+ const ms = Date.parse(v2);
2957
+ return Number.isNaN(ms) ? void 0 : ms;
2958
+ }
2959
+ return void 0;
2960
+ }
2961
+ function runTime(r) {
2962
+ return r.createdAt ?? r.updatedAt ?? Number.NEGATIVE_INFINITY;
2963
+ }
2964
+ function latestSucceededRun(runs) {
2965
+ let best;
2966
+ for (const r of runs) {
2967
+ if (r.status !== "succeeded") continue;
2968
+ if (best === void 0 || runTime(r) > runTime(best)) best = r;
2969
+ }
2970
+ return best;
2971
+ }
2953
2972
  async function fetchBoardStatus(base, fetchImpl, artifactId) {
2954
2973
  const boardKey = deriveBoardKey(artifactId);
2955
2974
  let boardExists = true;
@@ -2975,10 +2994,20 @@ async function fetchBoardStatus(base, fetchImpl, artifactId) {
2975
2994
  "GET",
2976
2995
  `/api/runs?projectId=${encodeURIComponent(boardKey)}`
2977
2996
  );
2978
- runs = (body2.runs ?? []).map((r) => ({
2979
- runId: String(r["runId"] ?? r["id"] ?? ""),
2980
- status: String(r["status"] ?? "unknown")
2981
- }));
2997
+ runs = (body2.runs ?? []).map((r) => {
2998
+ const run = {
2999
+ runId: String(r["runId"] ?? r["id"] ?? ""),
3000
+ status: String(r["status"] ?? "unknown")
3001
+ };
3002
+ if (typeof r["agentId"] === "string" && r["agentId"].length > 0) run.agentId = r["agentId"];
3003
+ if (typeof r["error"] === "string" && r["error"].trim().length > 0) run.error = r["error"];
3004
+ const createdAt = toEpochMs(r["createdAt"]);
3005
+ if (createdAt !== void 0) run.createdAt = createdAt;
3006
+ const updatedAt = toEpochMs(r["updatedAt"]);
3007
+ if (updatedAt !== void 0) run.updatedAt = updatedAt;
3008
+ return run;
3009
+ });
3010
+ runs.sort((a, b2) => runTime(b2) - runTime(a) || 0);
2982
3011
  } catch {
2983
3012
  }
2984
3013
  }
@@ -218354,13 +218383,22 @@ function cloneOffboarding(op) {
218354
218383
  assignmentDecisions: op.assignmentDecisions.map((d) => ({ ...d, toPrincipal: { ...d.toPrincipal } }))
218355
218384
  };
218356
218385
  }
218357
- var memberKey, scopedKey, bridgeKey, MemoryControlPlaneStore;
218386
+ var memberKey, scopedKey, bridgeKey, migrationRow, MemoryControlPlaneStore;
218358
218387
  var init_memory_control_plane_store = __esm({
218359
218388
  "../testkit/src/memory-control-plane-store.ts"() {
218360
218389
  "use strict";
218361
218390
  memberKey = (companyId, accountId) => `${companyId} ${accountId}`;
218362
218391
  scopedKey = (companyId, id) => `${companyId}\0${id}`;
218363
218392
  bridgeKey = (companyId, type, id) => `${companyId}\0${type}\0${id}`;
218393
+ migrationRow = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
218394
+ companyId,
218395
+ accountId,
218396
+ roleBefore,
218397
+ roleAfter,
218398
+ evidence: audit2.evidence,
218399
+ ...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
218400
+ appliedAt: audit2.appliedAt
218401
+ });
218364
218402
  MemoryControlPlaneStore = class {
218365
218403
  companies = /* @__PURE__ */ new Map();
218366
218404
  members = /* @__PURE__ */ new Map();
@@ -218387,6 +218425,8 @@ var init_memory_control_plane_store = __esm({
218387
218425
  // key = companyId\0type\0id
218388
218426
  offboardings = /* @__PURE__ */ new Map();
218389
218427
  // key = companyId\0operationId
218428
+ /** 存量身份映射审计(ADR D3.4):**只追加**的一条数组,插入序即写入序。 */
218429
+ roleMigrations = [];
218390
218430
  // —— 公司 ——
218391
218431
  async createCompany(c) {
218392
218432
  if (this.companies.has(c.id)) throw new Error(`company already exists: ${c.id}`);
@@ -218418,8 +218458,25 @@ var init_memory_control_plane_store = __esm({
218418
218458
  }
218419
218459
  if (patch.status !== void 0) c.status = patch.status;
218420
218460
  if (patch.onboardingVariant !== void 0) c.onboardingVariant = patch.onboardingVariant;
218461
+ if (patch.dissolvedAt !== void 0) {
218462
+ if (patch.dissolvedAt === null) delete c.dissolvedAt;
218463
+ else c.dissolvedAt = patch.dissolvedAt;
218464
+ }
218465
+ if (patch.dissolvedBy !== void 0) {
218466
+ if (patch.dissolvedBy === null) delete c.dissolvedBy;
218467
+ else c.dissolvedBy = patch.dissolvedBy;
218468
+ }
218421
218469
  this.companies.set(id, c);
218422
218470
  }
218471
+ async dissolveCompanyIfOwner(companyId, accountId, at2) {
218472
+ const c = this.companies.get(companyId);
218473
+ if (!c) return { ok: false, reason: "not_found" };
218474
+ const caller = this.members.get(memberKey(companyId, accountId));
218475
+ if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
218476
+ const next = { ...c, status: "suspended", dissolvedAt: at2, dissolvedBy: accountId };
218477
+ this.companies.set(companyId, next);
218478
+ return { ok: true, company: { ...next } };
218479
+ }
218423
218480
  // —— 成员 ——
218424
218481
  async upsertMember(m2) {
218425
218482
  const key2 = memberKey(m2.companyId, m2.accountId);
@@ -218461,6 +218518,21 @@ var init_memory_control_plane_store = __esm({
218461
218518
  this.members.delete(key2);
218462
218519
  return { ok: true, member: { ...target } };
218463
218520
  }
218521
+ async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
218522
+ const target = this.members.get(memberKey(companyId, accountId));
218523
+ if (!target) return { ok: false, reason: "not_found" };
218524
+ if (target.role === "owner") return { ok: false, reason: "owner_protected" };
218525
+ const releasedAgents = [];
218526
+ for (const [key2, e] of this.digitalEmployees) {
218527
+ if (e.companyId !== companyId || e.ownerAccountId !== accountId) continue;
218528
+ const { ownerAccountId: _cleared, ...rest } = e;
218529
+ this.digitalEmployees.set(key2, { ...rest, updatedAt: at2 });
218530
+ releasedAgents.push(e.id);
218531
+ }
218532
+ this.principalBridges.delete(bridgeKey(companyId, "human", accountId));
218533
+ this.members.delete(memberKey(companyId, accountId));
218534
+ return { ok: true, member: { ...target }, releasedAgents };
218535
+ }
218464
218536
  async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
218465
218537
  const key2 = memberKey(companyId, accountId);
218466
218538
  const target = this.members.get(key2);
@@ -218476,6 +218548,72 @@ var init_memory_control_plane_store = __esm({
218476
218548
  this.members.set(key2, updated);
218477
218549
  return { ok: true, member: { ...updated } };
218478
218550
  }
218551
+ async setSoleCompanyOwner(companyId, toAccountId, opts) {
218552
+ const target = this.members.get(memberKey(companyId, toAccountId));
218553
+ if (!target) return { ok: false, reason: "not_found" };
218554
+ if (opts?.requireCurrentOwner !== void 0) {
218555
+ const caller = this.members.get(memberKey(companyId, opts.requireCurrentOwner));
218556
+ if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
218557
+ }
218558
+ if (!target.onboardingCompletedAt && target.role !== "owner") {
218559
+ return { ok: false, reason: "target_not_listed" };
218560
+ }
218561
+ const acc = this.accounts.get(toAccountId);
218562
+ if (!acc || acc.status !== "active") return { ok: false, reason: "target_inactive" };
218563
+ const others = [];
218564
+ for (const m2 of this.members.values()) {
218565
+ if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
218566
+ }
218567
+ if (others.length === 0 && target.role === "owner") {
218568
+ return { ok: true, owner: { ...target }, demoted: [] };
218569
+ }
218570
+ const demoted = others.map((m2) => {
218571
+ const next = { ...m2, role: "admin" };
218572
+ this.members.set(memberKey(companyId, m2.accountId), next);
218573
+ return { ...next };
218574
+ });
218575
+ const owner = { ...target, role: "owner" };
218576
+ this.members.set(memberKey(companyId, toAccountId), owner);
218577
+ return { ok: true, owner: { ...owner }, demoted };
218578
+ }
218579
+ // —— 存量身份映射(ADR D3)——
218580
+ async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
218581
+ const target = this.members.get(memberKey(companyId, toAccountId));
218582
+ if (!target) return { ok: false, reason: "not_found" };
218583
+ const others = [];
218584
+ for (const m2 of this.members.values()) {
218585
+ if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
218586
+ }
218587
+ if (others.length === 0 && target.role === "owner") {
218588
+ return { ok: true, owner: { ...target }, demoted: [], auditRows: [] };
218589
+ }
218590
+ const auditRows = [];
218591
+ const demoted = others.map((m2) => {
218592
+ const next = { ...m2, role: "admin" };
218593
+ this.members.set(memberKey(companyId, m2.accountId), next);
218594
+ auditRows.push(migrationRow(companyId, m2.accountId, "owner", "admin", audit2));
218595
+ return { ...next };
218596
+ });
218597
+ if (target.role !== "owner") auditRows.push(migrationRow(companyId, toAccountId, target.role, "owner", audit2));
218598
+ const owner = { ...target, role: "owner" };
218599
+ this.members.set(memberKey(companyId, toAccountId), owner);
218600
+ for (const r of auditRows) this.roleMigrations.push({ ...r });
218601
+ return { ok: true, owner: { ...owner }, demoted, auditRows };
218602
+ }
218603
+ async setMemberRoleOnly(companyId, accountId, role) {
218604
+ const key2 = memberKey(companyId, accountId);
218605
+ const cur = this.members.get(key2);
218606
+ if (!cur) return false;
218607
+ this.members.set(key2, { ...cur, role });
218608
+ return true;
218609
+ }
218610
+ // —— 存量身份映射审计(ADR D3.4):只追加 ——
218611
+ async appendMemberRoleMigrations(rows) {
218612
+ for (const r of rows) this.roleMigrations.push({ ...r });
218613
+ }
218614
+ async listMemberRoleMigrations(companyId) {
218615
+ return this.roleMigrations.filter((r) => companyId === void 0 || r.companyId === companyId).map((r) => ({ ...r }));
218616
+ }
218479
218617
  // —— 邀请 ——
218480
218618
  async createInvitation(inv) {
218481
218619
  if (this.invitations.has(inv.id)) throw new Error(`invitation already exists: ${inv.id}`);
@@ -223753,9 +223891,16 @@ var init_control_plane_file_store = __esm({
223753
223891
  MUTATORS2 = [
223754
223892
  "createCompany",
223755
223893
  "updateCompany",
223894
+ "dissolveCompanyIfOwner",
223756
223895
  "upsertMember",
223757
223896
  "removeMember",
223758
223897
  "removeMemberIfNotLastOwner",
223898
+ "updateMemberRoleIfNotLastOwner",
223899
+ "setSoleCompanyOwner",
223900
+ "removeMemberAndReleaseOwnedAgents",
223901
+ "migrateSoleCompanyOwner",
223902
+ "setMemberRoleOnly",
223903
+ "appendMemberRoleMigrations",
223759
223904
  "createInvitation",
223760
223905
  "updateInvitationStatus",
223761
223906
  "updateInvitationStatusIf",
@@ -223807,6 +223952,7 @@ var init_control_plane_file_store = __esm({
223807
223952
  for (const c of snap.agentCredentials ?? []) await MemoryControlPlaneStore.prototype.putAgentCredential.call(store, c);
223808
223953
  for (const b2 of snap.principalBridges ?? []) await MemoryControlPlaneStore.prototype.putPrincipalBridge.call(store, b2);
223809
223954
  for (const op of snap.offboardings ?? []) store.offboardings.set(`${op.companyId}\0${op.operationId}`, op);
223955
+ for (const r of snap.roleMigrations ?? []) store.roleMigrations.push({ ...r });
223810
223956
  if (needsOnboardingBackfill) await store.save();
223811
223957
  } else {
223812
223958
  fs25.mkdirSync(path24.dirname(file), { recursive: true });
@@ -223844,7 +223990,8 @@ var init_control_plane_file_store = __esm({
223844
223990
  agentCredentials,
223845
223991
  principalBridges,
223846
223992
  // 交接操作无按公司列举的读面(按 operationId / accountId 定位),直接读 protected map。
223847
- offboardings: [...this.offboardings.values()]
223993
+ offboardings: [...this.offboardings.values()],
223994
+ roleMigrations: await this.listMemberRoleMigrations()
223848
223995
  };
223849
223996
  fs25.writeFileSync(this.file, JSON.stringify(snap, null, 2));
223850
223997
  }
@@ -225071,6 +225218,176 @@ var init_org_model_backfill = __esm({
225071
225218
  }
225072
225219
  });
225073
225220
 
225221
+ // ../server/src/org-identity-migration.ts
225222
+ function e1Candidate(members) {
225223
+ if (members.length === 0) return void 0;
225224
+ if (members.some((m2) => !m2.createdAt)) return void 0;
225225
+ return [...members].sort((a, b2) => {
225226
+ const ta = a.createdAt;
225227
+ const tb = b2.createdAt;
225228
+ if (ta !== tb) return ta < tb ? -1 : 1;
225229
+ return a.accountId < b2.accountId ? -1 : a.accountId > b2.accountId ? 1 : 0;
225230
+ })[0];
225231
+ }
225232
+ async function inventoryOrgIdentity(deps, opts = {}) {
225233
+ const listMembers = deps.listMembersWithCreatedAt ?? ((cid) => deps.store.listMembers(cid));
225234
+ const companies = (await deps.store.listCompanies()).filter((c) => opts.companyId === void 0 || c.id === opts.companyId);
225235
+ const rows = [];
225236
+ for (const c of companies) {
225237
+ const members = await listMembers(c.id);
225238
+ rows.push(decide(c, members, opts.manifest));
225239
+ }
225240
+ return {
225241
+ rows,
225242
+ blocked: rows.filter((r) => r.decision.startsWith("blocked")).length,
225243
+ pending: rows.filter((r) => (r.decision === "remap" || r.decision === "manifest") && r.changes.length > 0).length
225244
+ };
225245
+ }
225246
+ function decide(company, members, manifest) {
225247
+ const owners = members.filter((m2) => m2.role === "owner");
225248
+ const base = {
225249
+ companyId: company.id,
225250
+ companyName: company.name,
225251
+ status: company.status,
225252
+ members: members.length,
225253
+ owners: owners.length,
225254
+ admins: members.filter((m2) => m2.role === "admin").length,
225255
+ plainMembers: members.filter((m2) => m2.role === "member").length,
225256
+ rowsWithoutCreatedAt: members.filter((m2) => !m2.createdAt).length,
225257
+ changes: []
225258
+ };
225259
+ if (members.length === 0) {
225260
+ 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" };
225261
+ }
225262
+ const pinned = manifest?.owners[company.id];
225263
+ if (pinned !== void 0) {
225264
+ if (!members.some((m2) => m2.accountId === pinned)) {
225265
+ return {
225266
+ ...base,
225267
+ decision: "blocked:no-owner",
225268
+ 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`
225269
+ };
225270
+ }
225271
+ return {
225272
+ ...base,
225273
+ decision: "manifest",
225274
+ targetOwnerAccountId: pinned,
225275
+ changes: plannedChanges(members, pinned),
225276
+ reason: `\u6309\u51BB\u7ED3 manifest\uFF08${manifest.manifestVersion}\uFF09\u6307\u5B9A ${pinned} \u4E3A\u7EC4\u7EC7\u7BA1\u7406\u5458`
225277
+ };
225278
+ }
225279
+ const e1 = e1Candidate(members);
225280
+ if (!e1) {
225281
+ return {
225282
+ ...base,
225283
+ decision: "blocked:no-created-at",
225284
+ 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`
225285
+ };
225286
+ }
225287
+ if (e1.role === "member") {
225288
+ return {
225289
+ ...base,
225290
+ decision: "blocked:e1-is-plain-member",
225291
+ e1AccountId: e1.accountId,
225292
+ 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`
225293
+ };
225294
+ }
225295
+ if (owners.length === 0) {
225296
+ return {
225297
+ ...base,
225298
+ decision: "blocked:no-owner",
225299
+ e1AccountId: e1.accountId,
225300
+ 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"
225301
+ };
225302
+ }
225303
+ const changes = plannedChanges(members, e1.accountId);
225304
+ if (changes.length === 0) {
225305
+ return {
225306
+ ...base,
225307
+ decision: "ok",
225308
+ e1AccountId: e1.accountId,
225309
+ targetOwnerAccountId: e1.accountId,
225310
+ 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"
225311
+ };
225312
+ }
225313
+ return {
225314
+ ...base,
225315
+ decision: "remap",
225316
+ e1AccountId: e1.accountId,
225317
+ targetOwnerAccountId: e1.accountId,
225318
+ changes,
225319
+ 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`
225320
+ };
225321
+ }
225322
+ function plannedChanges(members, targetOwner) {
225323
+ const out = [];
225324
+ for (const m2 of members) {
225325
+ if (m2.accountId === targetOwner) {
225326
+ if (m2.role !== "owner") out.push({ accountId: m2.accountId, roleBefore: m2.role, roleAfter: "owner" });
225327
+ continue;
225328
+ }
225329
+ if (m2.role === "owner") out.push({ accountId: m2.accountId, roleBefore: "owner", roleAfter: "admin" });
225330
+ }
225331
+ return out;
225332
+ }
225333
+ async function backfillOrgIdentity(deps, opts = {}) {
225334
+ const log3 = opts.log ?? (() => {
225335
+ });
225336
+ const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
225337
+ const inv = await inventoryOrgIdentity(deps, opts);
225338
+ let applied = 0;
225339
+ let auditRows = 0;
225340
+ for (const row of inv.rows) {
225341
+ if (row.decision !== "remap" && row.decision !== "manifest") {
225342
+ if (row.decision.startsWith("blocked")) log3(` \u26D4 ${row.companyId} ${row.decision}\uFF1A${row.reason}`);
225343
+ continue;
225344
+ }
225345
+ if (row.changes.length === 0) continue;
225346
+ const target = row.targetOwnerAccountId;
225347
+ const desc = row.changes.map((c) => `${c.accountId} ${c.roleBefore}\u2192${c.roleAfter}`).join("\uFF0C");
225348
+ log3(` ${row.companyId}\uFF1A${desc}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225349
+ if (!opts.apply) continue;
225350
+ const evidence = row.decision === "manifest" ? "manifest" : "E1";
225351
+ const res = await deps.store.migrateSoleCompanyOwner(row.companyId, target, {
225352
+ evidence,
225353
+ ...opts.manifest?.manifestVersion !== void 0 ? { manifestVersion: opts.manifest.manifestVersion } : {},
225354
+ appliedAt: now()
225355
+ });
225356
+ if (!res.ok) {
225357
+ throw new Error(`${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08\u90A3\u4E00\u884C\u4E0D\u5728\u8BE5\u7EC4\u7EC7\u7684\u6210\u5458\u8868\u91CC\uFF09`);
225358
+ }
225359
+ applied += 1;
225360
+ auditRows += res.auditRows.length;
225361
+ }
225362
+ return { ...inv, applied, auditRows };
225363
+ }
225364
+ async function rollbackOrgIdentity(deps, opts) {
225365
+ const log3 = opts.log ?? (() => {
225366
+ });
225367
+ const audit2 = await deps.store.listMemberRoleMigrations(opts.companyId);
225368
+ const plan = [];
225369
+ let reverted = 0;
225370
+ for (const r of [...audit2].reverse()) {
225371
+ const member = await deps.store.getMember(r.companyId, r.accountId);
225372
+ if (!member) {
225373
+ log3(` \u8DF3\u8FC7 ${r.accountId}\uFF1A\u6210\u5458\u884C\u5DF2\u4E0D\u5B58\u5728\uFF08\u88AB\u79FB\u9664\u8FC7\uFF09`);
225374
+ continue;
225375
+ }
225376
+ if (member.role === r.roleBefore) continue;
225377
+ plan.push({ companyId: r.companyId, accountId: r.accountId, from: member.role, to: r.roleBefore });
225378
+ log3(` ${r.companyId} ${r.accountId} ${member.role}\u2192${r.roleBefore}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
225379
+ if (!opts.apply) continue;
225380
+ await deps.store.setMemberRoleOnly(r.companyId, r.accountId, r.roleBefore);
225381
+ reverted += 1;
225382
+ }
225383
+ return { plan, reverted };
225384
+ }
225385
+ var init_org_identity_migration = __esm({
225386
+ "../server/src/org-identity-migration.ts"() {
225387
+ "use strict";
225388
+ }
225389
+ });
225390
+
225074
225391
  // ../server/src/config-audit.ts
225075
225392
  function shallowDiff(before, after) {
225076
225393
  const out = {};
@@ -232382,6 +232699,68 @@ var init_runtime_tenancy = __esm({
232382
232699
  }
232383
232700
  });
232384
232701
 
232702
+ // ../server/src/governance/agent-manage-guard.ts
232703
+ function isCompanyManager(role) {
232704
+ return role === "owner" || role === "admin";
232705
+ }
232706
+ function isAssistantAgent(subject) {
232707
+ return subject.positions.includes(ASSISTANT_POSITION);
232708
+ }
232709
+ function mayManageAgent(caller, subject) {
232710
+ if (!isAssistantAgent(subject)) return isCompanyManager(caller.role);
232711
+ if (subject.boundHumanActorId === void 0) return isCompanyManager(caller.role);
232712
+ if (subject.boundHumanActorId === caller.actorId) return true;
232713
+ if (subject.boundHumanReportsTo !== void 0 && subject.boundHumanReportsTo === caller.actorId) return true;
232714
+ return isCompanyManager(caller.role);
232715
+ }
232716
+ async function assertMayManageAgent(guard, ctx, agent) {
232717
+ if (!guard) return;
232718
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232719
+ const subject = { positions: agent.positions };
232720
+ if (isAssistantAgent(subject)) {
232721
+ const human = await guard.boundHuman(ctx.companyId, agent.id);
232722
+ if (human) {
232723
+ subject.boundHumanActorId = human;
232724
+ subject.boundHumanReportsTo = await guard.reportsTo(ctx.companyId, human) ?? void 0;
232725
+ }
232726
+ }
232727
+ if (mayManageAgent({ role, actorId: ctx.callerActorId }, subject)) return;
232728
+ throw new AgentManageForbiddenError(
232729
+ 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"
232730
+ );
232731
+ }
232732
+ async function assertCompanyManager(guard, ctx, what) {
232733
+ if (!guard) return;
232734
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232735
+ if (isCompanyManager(role)) return;
232736
+ throw new AgentManageForbiddenError(`\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5${what}`);
232737
+ }
232738
+ async function assertMayCreateAgent(guard, ctx) {
232739
+ return assertCompanyManager(guard, ctx, "\u65B0\u5EFA Agent");
232740
+ }
232741
+ async function assertMaySelfOrManage(guard, ctx, targetActorId) {
232742
+ if (!guard) return;
232743
+ if (ctx.callerActorId === targetActorId) return;
232744
+ const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
232745
+ if (isCompanyManager(role)) return;
232746
+ 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");
232747
+ }
232748
+ var ASSISTANT_POSITION, AgentManageForbiddenError;
232749
+ var init_agent_manage_guard = __esm({
232750
+ "../server/src/governance/agent-manage-guard.ts"() {
232751
+ "use strict";
232752
+ ASSISTANT_POSITION = "assistant";
232753
+ AgentManageForbiddenError = class extends Error {
232754
+ status = 403;
232755
+ code = "FORBIDDEN";
232756
+ constructor(message) {
232757
+ super(message);
232758
+ this.name = "AgentManageForbiddenError";
232759
+ }
232760
+ };
232761
+ }
232762
+ });
232763
+
232385
232764
  // ../server/src/domains/actors/routes.ts
232386
232765
  async function asApiError(run) {
232387
232766
  try {
@@ -232470,6 +232849,33 @@ function mapMarketError(err) {
232470
232849
  }
232471
232850
  function actorsDomain(opts) {
232472
232851
  const { resolveCtx } = opts;
232852
+ const asForbidden = async (run) => {
232853
+ try {
232854
+ await run();
232855
+ } catch (e) {
232856
+ if (e instanceof AgentManageForbiddenError) throw new ApiError(e.status, e.code, e.message);
232857
+ throw e;
232858
+ }
232859
+ };
232860
+ const guardAgentWrite = async (req, actor) => {
232861
+ const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
232862
+ if (actor.kind !== "agent") {
232863
+ await asForbidden(() => assertMaySelfOrManage(opts.agentManageGuard, ctx, actor.id));
232864
+ return;
232865
+ }
232866
+ await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
232867
+ };
232868
+ const guardPositionsChange = async (req, existing, nextPositions) => {
232869
+ if (existing.kind !== "agent" || nextPositions === void 0) return;
232870
+ if (isAssistantAgent({ positions: existing.roles }) === isAssistantAgent({ positions: nextPositions })) return;
232871
+ const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
232872
+ await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: existing.id, positions: nextPositions }));
232873
+ };
232874
+ const guardExistingActorWrite = async (req, service, actorId) => {
232875
+ const existing = await service.getActor(actorId);
232876
+ if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
232877
+ await guardAgentWrite(req, existing);
232878
+ };
232473
232879
  const revealLimits = opts.credentialRevealLimits ?? { perSession: 200, perSessionPerKey: 20 };
232474
232880
  const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
232475
232881
  const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
@@ -232522,6 +232928,10 @@ function actorsDomain(opts) {
232522
232928
  const { service } = await resolveCtx(req.auth.companyId);
232523
232929
  const b2 = req.body;
232524
232930
  if (!b2?.id || !b2.kind || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / kind / name");
232931
+ await asForbidden(() => assertMayCreateAgent(opts.agentManageGuard, {
232932
+ companyId: req.auth.companyId,
232933
+ callerActorId: req.auth.actor
232934
+ }));
232525
232935
  const { id, kind, name } = b2;
232526
232936
  const positions = positionsFromPayload(b2);
232527
232937
  const avatar = avatarRefFromPayload(b2);
@@ -232548,10 +232958,12 @@ function actorsDomain(opts) {
232548
232958
  const { service } = await resolveCtx(req.auth.companyId);
232549
232959
  const existing = await service.getActor(req.params.id);
232550
232960
  if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${req.params.id}`);
232961
+ await guardAgentWrite(req, existing);
232551
232962
  const b2 = req.body ?? {};
232552
232963
  const positions = positionsFromPayload(b2);
232553
232964
  const avatar = avatarRefFromPayload(b2);
232554
232965
  const tags = tagsFromPayload(b2);
232966
+ await guardPositionsChange(req, existing, positions);
232555
232967
  const {
232556
232968
  positions: _positions,
232557
232969
  id: _id,
@@ -232582,23 +232994,27 @@ function actorsDomain(opts) {
232582
232994
  });
232583
232995
  router.delete("/api/actors/:id", async (req) => {
232584
232996
  const { service } = await resolveCtx(req.auth.companyId);
232997
+ await guardExistingActorWrite(req, service, req.params.id);
232585
232998
  await service.disableActor(req.params.id, req.auth.actor);
232586
232999
  return { status: 200, body: { ok: true } };
232587
233000
  });
232588
233001
  router.post("/api/actors/:id/avatar", async (req) => {
232589
233002
  const { service, blobs } = await resolveCtx(req.auth.companyId);
232590
233003
  if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
233004
+ await guardExistingActorWrite(req, service, req.params.id);
232591
233005
  const ref2 = await storeImageBlob(blobs, req.body?.data);
232592
233006
  const actor = await service.setActorAvatar(req.params.id, ref2, req.auth.actor);
232593
233007
  return { status: 201, body: { avatar: actor.avatar } };
232594
233008
  });
232595
233009
  router.delete("/api/actors/:id/avatar", async (req) => {
232596
233010
  const { service } = await resolveCtx(req.auth.companyId);
233011
+ await guardExistingActorWrite(req, service, req.params.id);
232597
233012
  await service.setActorAvatar(req.params.id, null, req.auth.actor);
232598
233013
  return { status: 200, body: { ok: true } };
232599
233014
  });
232600
233015
  router.post("/api/teams/:id/icon", async (req) => {
232601
233016
  const { service, blobs } = await resolveCtx(req.auth.companyId);
233017
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
232602
233018
  if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
232603
233019
  const ref2 = await storeImageBlob(blobs, req.body?.data);
232604
233020
  await service.setTeamIcon(req.params.id, ref2);
@@ -232606,6 +233022,7 @@ function actorsDomain(opts) {
232606
233022
  });
232607
233023
  router.delete("/api/teams/:id/icon", async (req) => {
232608
233024
  const { service } = await resolveCtx(req.auth.companyId);
233025
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
232609
233026
  await service.setTeamIcon(req.params.id, null);
232610
233027
  return { status: 200, body: { ok: true } };
232611
233028
  });
@@ -232617,6 +233034,7 @@ function actorsDomain(opts) {
232617
233034
  });
232618
233035
  router.post("/api/actors/:id/config", async (req) => {
232619
233036
  const { service } = await resolveCtx(req.auth.companyId);
233037
+ await guardExistingActorWrite(req, service, req.params.id);
232620
233038
  const b2 = req.body ?? {};
232621
233039
  const checkString = (key2) => {
232622
233040
  if (b2[key2] !== void 0 && typeof b2[key2] !== "string") {
@@ -232678,6 +233096,7 @@ function actorsDomain(opts) {
232678
233096
  const { service } = await resolveCtx(req.auth.companyId);
232679
233097
  const actorId = req.params.id;
232680
233098
  if (!await service.getActor(actorId)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
233099
+ await guardExistingActorWrite(req, service, actorId);
232681
233100
  const raw = req.body?.candidates;
232682
233101
  if (!Array.isArray(raw)) throw new ApiError(400, "BAD_REQUEST", "candidates \u5FC5\u987B\u662F\u6570\u7EC4");
232683
233102
  if (raw.length > 20) throw new ApiError(400, "BAD_REQUEST", "\u5019\u9009\u8FD0\u884C\u65F6\u6700\u591A 20 \u6761");
@@ -232712,6 +233131,7 @@ function actorsDomain(opts) {
232712
233131
  });
232713
233132
  router.post("/api/actors/:id/config/rollback", async (req) => {
232714
233133
  const { service } = await resolveCtx(req.auth.companyId);
233134
+ await guardExistingActorWrite(req, service, req.params.id);
232715
233135
  const b2 = req.body;
232716
233136
  if (typeof b2?.version !== "number") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 version");
232717
233137
  const cfg = await service.rollbackConfig(req.params.id, b2.version, req.auth.actor);
@@ -232723,6 +233143,7 @@ function actorsDomain(opts) {
232723
233143
  });
232724
233144
  router.patch("/api/actors/:id/settings", async (req) => {
232725
233145
  const { service } = await resolveCtx(req.auth.companyId);
233146
+ await guardExistingActorWrite(req, service, req.params.id);
232726
233147
  const b2 = req.body ?? {};
232727
233148
  if (b2.memoryEnabled !== void 0 && typeof b2.memoryEnabled !== "boolean") {
232728
233149
  throw new ApiError(400, "BAD_REQUEST", "memoryEnabled \u5FC5\u987B\u662F boolean");
@@ -232742,6 +233163,7 @@ function actorsDomain(opts) {
232742
233163
  const { service } = await resolveCtx(req.auth.companyId);
232743
233164
  const b2 = req.body;
232744
233165
  if (!b2?.id || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / name");
233166
+ await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u65B0\u5EFA\u56E2\u961F"));
232745
233167
  await service.upsertTeam({
232746
233168
  id: b2.id,
232747
233169
  name: b2.name,
@@ -232762,6 +233184,7 @@ function actorsDomain(opts) {
232762
233184
  const { service } = await resolveCtx(req.auth.companyId);
232763
233185
  const b2 = req.body;
232764
233186
  if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
233187
+ await guardExistingActorWrite(req, service, b2.actorId);
232765
233188
  const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
232766
233189
  const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
232767
233190
  if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
@@ -232770,6 +233193,7 @@ function actorsDomain(opts) {
232770
233193
  });
232771
233194
  router.delete("/api/bindings/:actorId", async (req) => {
232772
233195
  const { service } = await resolveCtx(req.auth.companyId);
233196
+ await guardExistingActorWrite(req, service, req.params.actorId);
232773
233197
  await service.unbind(req.params.actorId, req.auth.actor);
232774
233198
  return { status: 200, body: { ok: true } };
232775
233199
  });
@@ -232805,6 +233229,7 @@ function actorsDomain(opts) {
232805
233229
  });
232806
233230
  router.put("/api/actors/:id/connectors/:connectorId", async (req) => {
232807
233231
  const { service } = await resolveCtx(req.auth.companyId);
233232
+ await guardExistingActorWrite(req, service, req.params.id);
232808
233233
  const b2 = req.body;
232809
233234
  if (typeof b2?.enabled !== "boolean") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 enabled (boolean)");
232810
233235
  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 +234398,7 @@ var init_routes3 = __esm({
233973
234398
  init_skill_materializer();
233974
234399
  init_connector_skills();
233975
234400
  init_runtime_tenancy();
234401
+ init_agent_manage_guard();
233976
234402
  CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
233977
234403
  RevealRateLimiter = class {
233978
234404
  constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
@@ -234304,7 +234730,7 @@ function createActorsDomain(opts) {
234304
234730
  return {
234305
234731
  service: defaultCtx.service,
234306
234732
  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 } : {} })
234733
+ 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
234734
  };
234309
234735
  }
234310
234736
  var import_node_crypto55;
@@ -241577,11 +242003,34 @@ var init_service6 = __esm({
241577
242003
  async isEmployedElsewhere(companyId, accountId) {
241578
242004
  return isEmployedElsewhere(this.store, companyId, accountId);
241579
242005
  }
241580
- /** 当前用户所属的公司(按 membership 聚合)。 */
242006
+ /**
242007
+ * 当前用户所属的公司(按 membership 聚合)。**只返回 `status='active'` 的**。
242008
+ *
242009
+ * 🚨 过滤 status 是「解散组织」能真正生效的一半(组织三层身份 ADR D9.2):解散取的是可审计的
242010
+ * 停用语义(`status='suspended'` + `dissolved_at/by`),成员行**保留**。不在这里过滤的话,
242011
+ * 被解散的组织仍然会出现在顶栏切换器上、`CompanyGate` 也仍把这个人算作「有组织」——
242012
+ * 解散就只是换了个状态字,人照进不误。口径与 `resolveCurrentCompany` 的无 selector 分支对齐
242013
+ * (那一支本来就只挑 active)。某人全部组织都被解散后,按既有逻辑落到建团队页。
242014
+ */
241581
242015
  async listForUser(accountId) {
241582
242016
  const memberships = await this.store.listMembershipsForAccount(accountId);
241583
242017
  const fetched = await Promise.all(memberships.map((m2) => this.store.getCompany(m2.companyId)));
241584
- return fetched.filter((c) => c !== null && c !== void 0);
242018
+ return fetched.filter((c) => c !== null && c !== void 0 && c.status === "active");
242019
+ }
242020
+ /**
242021
+ * 「我在哪些组织里还是组织管理员」——注销账号的跨组织检查(ADR D9.3)。
242022
+ * 只看 `status='active'` 的组织:已解散的那家里是不是 owner 已经无关紧要。
242023
+ * **不能只看当前组织**:注销是账号级动作,漏一家就等于把那家组织甩成无人可管。
242024
+ */
242025
+ async listOwnedActiveCompanies(accountId) {
242026
+ const memberships = await this.store.listMembershipsForAccount(accountId);
242027
+ const out = [];
242028
+ for (const m2 of memberships) {
242029
+ if (m2.role !== "owner") continue;
242030
+ const c = await this.store.getCompany(m2.companyId);
242031
+ if (c?.status === "active") out.push(c);
242032
+ }
242033
+ return out;
241585
242034
  }
241586
242035
  async get(id) {
241587
242036
  return this.store.getCompany(id);
@@ -241646,11 +242095,49 @@ var init_service6 = __esm({
241646
242095
  return account;
241647
242096
  }
241648
242097
  /**
241649
- * 授予某邮箱在某公司的 owner(ADR 0009 切片5:bootstrap 种子 + grant-owner 兜底)。
241650
- * 确保账号存在 + upsert owner 成员。幂等(重复调把其角色置 owner)。
242098
+ * 授予某邮箱在某公司的组织管理员(ADR 0009 切片5:bootstrap 种子 + `grant-owner` 兜底)。
242099
+ *
242100
+ * 🚨 **它必须自己守 I1**(组织三层身份 ADR D2.4)。老实现是**无条件** upsert `role='owner'`,
242101
+ * 而调用点之一是 serve 的 bootstrap(`OASIS_BOOTSTRAP_OWNER`,在**启动路径**上)——该组织若已有
242102
+ * 组织管理员,它会写出第二位,在部分唯一索引 `company_members_one_owner_idx` 下直接抛错,
242103
+ * **等于 serve 起不来**。所以三分支:
242104
+ * - 目标已是该组织 owner → no-op,返回既有那行;
242105
+ * - 组织已有**别的** owner → **不写 owner**、`console.warn`,返回 `{ granted: false, existingOwner }`,
242106
+ * CLI 侧据此非零退出。但**仍然保证他是成员**:还不是成员就落一条 `admin`(已是成员的角色一个字不动)。
242107
+ * 为什么不是干脆「什么都不写」:这条通道的本意是 ADR 0009 的「第一个 owner 不会把自己锁在门外」
242108
+ * ——`OASIS_BOOTSTRAP_OWNER_EMAIL` 要让创始人**能登录并进得去管理面**。什么都不写的话,
242109
+ * 首装时若默认公司迁移已经指派过一位 owner,创始人就只有账号、没有任何 membership,
242110
+ * 登录后被 `CompanyGate` 扔到建团队页——兜底通道当场失效。管理面的闸是 `requireOwnerOrAdmin`,
242111
+ * admin 一样进得去;而「谁是唯一持有人」不该被一次装机悄悄换人。
242112
+ * - 组织无 owner(首次 bootstrap、或存量 0-owner 的脏数据)→ 照旧指派为 owner。
242113
+ * 「换人」一律走转交(`POST /api/companies/:id/owner`),不从这条兜底通道走。
241651
242114
  */
241652
242115
  async grantOwner(companyId, email2, name) {
241653
242116
  const account = await this.ensureAccount(email2, name);
242117
+ const members = await this.store.listMembers(companyId);
242118
+ const currentOwner = members.find((m2) => m2.role === "owner");
242119
+ if (currentOwner && currentOwner.accountId !== account.id) {
242120
+ const existingSelf = members.find((m2) => m2.accountId === account.id);
242121
+ if (existingSelf) {
242122
+ console.warn(
242123
+ `[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`
242124
+ );
242125
+ return { granted: false, membership: existingSelf, existingOwner: currentOwner };
242126
+ }
242127
+ const asAdmin = {
242128
+ companyId,
242129
+ accountId: account.id,
242130
+ actorId: account.id,
242131
+ role: "admin",
242132
+ onboardingCompletedAt: this.now()
242133
+ };
242134
+ await this.store.upsertMember(asAdmin);
242135
+ console.warn(
242136
+ `[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`
242137
+ );
242138
+ return { granted: false, membership: asAdmin, existingOwner: currentOwner };
242139
+ }
242140
+ if (currentOwner) return { granted: false, membership: currentOwner };
241654
242141
  const membership = {
241655
242142
  companyId,
241656
242143
  accountId: account.id,
@@ -241659,7 +242146,7 @@ var init_service6 = __esm({
241659
242146
  onboardingCompletedAt: this.now()
241660
242147
  };
241661
242148
  await this.store.upsertMember(membership);
241662
- return membership;
242149
+ return { granted: true, membership };
241663
242150
  }
241664
242151
  /**
241665
242152
  * 当前公司解析(动态、随登录用户走):
@@ -241672,6 +242159,7 @@ var init_service6 = __esm({
241672
242159
  let company;
241673
242160
  if (selector) {
241674
242161
  company = await this.store.getCompany(selector) ?? await this.store.getCompanyBySlug(selector);
242162
+ if (company && company.status !== "active") company = null;
241675
242163
  } else {
241676
242164
  company = null;
241677
242165
  for (const m2 of await this.store.listMembershipsForAccount(accountId)) {
@@ -241774,11 +242262,91 @@ var init_service6 = __esm({
241774
242262
  */
241775
242263
  async changeMemberRole(companyId, accountId, role) {
241776
242264
  if (!ROLES.includes(role)) throw new CompanyError(400, "BAD_ROLE", `role \u987B\u4E3A ${ROLES.join(" | ")}`);
242265
+ if (role === "owner") {
242266
+ 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");
242267
+ }
241777
242268
  await this.requireCompany(companyId);
242269
+ const target = await this.store.getMember(companyId, accountId);
242270
+ if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242271
+ if (target.role === "owner") throw this.ownerProtected();
241778
242272
  const res = await this.store.updateMemberRoleIfNotLastOwner(companyId, accountId, role);
241779
242273
  if (res.ok) return res.member;
241780
242274
  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");
242275
+ throw this.ownerProtected();
242276
+ }
242277
+ /** 「组织管理员受保护」的唯一错误来源——成员管理面的四个入口共用,码与文案不各写一份。 */
242278
+ ownerProtected() {
242279
+ 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");
242280
+ }
242281
+ /**
242282
+ * 转交组织管理员(`POST /api/companies/:id/owner`,ADR D4)。
242283
+ *
242284
+ * 调用者身份从会话解析、**不接受 body 里指定 from**。单事务、一把锁、**先降后升**,
242285
+ * 全部由 `store.setSoleCompanyOwner` 承担(并发语义见它的契约注释)。
242286
+ *
242287
+ * **刻意不做「已达成即 200」的幂等吞掉**:同一转交被重复提交时第二次同样得 409 `NOT_OWNER`。
242288
+ * 吞掉的话「我不是 owner 了」和「我刚转交成功」在响应里长得一样,前端分不出该显示
242289
+ * 「转交成功」还是「你的身份已变更」。
242290
+ *
242291
+ * **不 bump `tokenVersion`**:角色每请求现查(D7),转交不需要作废会话;bump 会把人从
242292
+ * **其它组织**一起踢下线(G10)。
242293
+ */
242294
+ async transferOwner(companyId, fromAccountId, toAccountId) {
242295
+ if (!toAccountId) throw new CompanyError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId");
242296
+ if (fromAccountId === toAccountId) {
242297
+ throw new CompanyError(400, "SELF_TRANSFER", "\u4E0D\u80FD\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u81EA\u5DF1");
242298
+ }
242299
+ await this.requireActiveCompany(companyId);
242300
+ const res = await this.store.setSoleCompanyOwner(companyId, toAccountId, { requireCurrentOwner: fromAccountId });
242301
+ if (!res.ok) {
242302
+ if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
242303
+ if (res.reason === "target_not_listed") {
242304
+ 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");
242305
+ }
242306
+ if (res.reason === "target_inactive") {
242307
+ throw new CompanyError(409, "TARGET_INACTIVE", "\u5BF9\u65B9\u8D26\u53F7\u5DF2\u505C\u7528\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
242308
+ }
242309
+ 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");
242310
+ }
242311
+ const from = res.demoted.find((m2) => m2.accountId === fromAccountId);
242312
+ 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");
242313
+ return { from, to: res.owner };
242314
+ }
242315
+ /**
242316
+ * 存量映射用的「把这家组织收敛成唯一组织管理员」(ADR D3 回填面)。
242317
+ * 与转交**同一把锁、同一顺序**(先降后升)——两份实现必然漂,而漂出来的差异正好落在
242318
+ * 「中途有没有出现两位持有人」这种只有并发下才现形的地方。
242319
+ */
242320
+ async setSoleOwner(companyId, toAccountId) {
242321
+ const res = await this.store.setSoleCompanyOwner(companyId, toAccountId);
242322
+ if (!res.ok) {
242323
+ if (res.reason === "target_not_listed") {
242324
+ throw new CompanyError(409, "TARGET_NOT_LISTED", `\u76EE\u6807\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF1A${toAccountId}`);
242325
+ }
242326
+ if (res.reason === "target_inactive") {
242327
+ throw new CompanyError(409, "TARGET_INACTIVE", `\u76EE\u6807\u8D26\u53F7\u5DF2\u505C\u7528\uFF1A${toAccountId}`);
242328
+ }
242329
+ throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
242330
+ }
242331
+ return { owner: res.owner, demoted: res.demoted };
242332
+ }
242333
+ /**
242334
+ * 解散组织(`DELETE /api/companies/:id`,ADR D9.2)——**可审计的停用,不是物理销毁**。
242335
+ * 仅当前组织管理员;「我还是不是 owner」在**同一把锁内**复核(D7.2),不是入口查一次就写。
242336
+ * 断访问靠 `resolveCurrentCompany` 只认 active(D8.4)+ `listForUser` 只列 active(D9.2)。
242337
+ */
242338
+ async dissolveCompany(companyId, callerAccountId) {
242339
+ await this.requireActiveCompany(companyId);
242340
+ const res = await this.store.dissolveCompanyIfOwner(companyId, callerAccountId, this.now());
242341
+ if (res.ok) return res.company;
242342
+ if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
242343
+ throw new CompanyError(403, "FORBIDDEN", "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EF\u4EE5\u89E3\u6563\u7EC4\u7EC7");
242344
+ }
242345
+ /** 公司必须存在**且 active**:已解散 / 已停用的组织上不接受任何写。 */
242346
+ async requireActiveCompany(companyId) {
242347
+ const c = await this.requireCompany(companyId);
242348
+ if (c.status !== "active") throw new CompanyError(404, "COMPANY_NOT_FOUND", `\u516C\u53F8\u4E0D\u53EF\u7528\uFF1A${companyId}`);
242349
+ return c;
241782
242350
  }
241783
242351
  /**
241784
242352
  * 改成员在本公司的昵称(组织模型 ADR §4.1 `company_member.display_name`)。
@@ -241805,9 +242373,7 @@ var init_service6 = __esm({
241805
242373
  await this.requireCompany(companyId);
241806
242374
  const member = await this.store.getMember(companyId, accountId);
241807
242375
  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
- }
242376
+ if (member.role === "owner") throw this.ownerProtected();
241811
242377
  return member;
241812
242378
  }
241813
242379
  /**
@@ -241825,10 +242391,82 @@ var init_service6 = __esm({
241825
242391
  */
241826
242392
  async removeMember(companyId, accountId) {
241827
242393
  await this.requireCompany(companyId);
241828
- const res = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
241829
- if (res.ok) return res.member;
242394
+ const target = await this.store.getMember(companyId, accountId);
242395
+ if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242396
+ if (target.role === "owner") throw this.ownerProtected();
242397
+ const res = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now());
242398
+ if (res.ok) return { member: res.member, releasedAgents: res.releasedAgents };
241830
242399
  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");
242400
+ throw this.ownerProtected();
242401
+ }
242402
+ /**
242403
+ * 退出组织(`DELETE /api/companies/:id/members/me`,ADR D9.1)——**本人限定**。
242404
+ *
242405
+ * 组织管理员退不出:先转交、或(只剩他一个人时)解散。响应体带 `canDissolve`,
242406
+ * 前端据它把人引到「解散组织」而不是「先转交给谁」(brief:单人团队路径)。
242407
+ * 真正的移除副作用由 `MemberOffboardingService.applyMemberRemoval` 承担——与管理员移除他人
242408
+ * **同一套**,不在这里另写一份。
242409
+ */
242410
+ async peekSelfLeave(companyId, accountId) {
242411
+ await this.requireActiveCompany(companyId);
242412
+ const me = await this.store.getMember(companyId, accountId);
242413
+ if (!me) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242414
+ if (me.role === "owner") {
242415
+ const canDissolve = (await this.store.listMembers(companyId)).length <= 1;
242416
+ throw new CompanyError(
242417
+ 409,
242418
+ "OWNER_MUST_TRANSFER",
242419
+ 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",
242420
+ { canDissolve }
242421
+ );
242422
+ }
242423
+ return me;
242424
+ }
242425
+ /**
242426
+ * 注销账号的**跨组织前置检查**(`DELETE /api/accounts/me`,ADR D9.3)。
242427
+ *
242428
+ * 遍历该账号**全部** membership,取其中 `status='active'` 的组织:只要**任意一家**里还是
242429
+ * 组织管理员就拒(409 `OWNER_IN_ORGS`,响应体列出这些组织的 `{id, name}`),要求先转交或解散。
242430
+ * 🚨 这条检查天然跨组织、与「当前公司」无关——**不能**只看当前组织,漏一家就等于把那家组织
242431
+ * 甩成无人可管,而那个人已经注销、再也转交不了。
242432
+ *
242433
+ * 返回值 = 要逐家执行退出副作用的 membership 清单。**只含 active 组织**:已解散组织里的成员行
242434
+ * 原样保留(账号停用后进不去,解散组织本身也解析不出来),不为了「删干净」去碰一家已经归档的
242435
+ * 组织的成员名单——那会让它的历史成员名册凭空少人。
242436
+ */
242437
+ async peekAccountDeletion(accountId) {
242438
+ const account = await this.store.getAccount(accountId);
242439
+ if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
242440
+ const owned = await this.listOwnedActiveCompanies(accountId);
242441
+ if (owned.length > 0) {
242442
+ throw new CompanyError(
242443
+ 409,
242444
+ "OWNER_IN_ORGS",
242445
+ "\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",
242446
+ { companies: owned.map((c) => ({ id: c.id, name: c.name })) }
242447
+ );
242448
+ }
242449
+ const memberships = await this.store.listMembershipsForAccount(accountId);
242450
+ const out = [];
242451
+ for (const m2 of memberships) {
242452
+ const c = await this.store.getCompany(m2.companyId);
242453
+ if (c?.status === "active") out.push(m2);
242454
+ }
242455
+ return out;
242456
+ }
242457
+ /**
242458
+ * 注销的最后一步:账号置 `disabled` + **bump `tokenVersion`**,当场作废该账号全部会话。
242459
+ * 这是 `tokenVersion` 的**正确**用法(账号级总闸);转交与降级**不得**用它——那会把人从
242460
+ * 其它组织一起踢下线(G10)。
242461
+ *
242462
+ * **数据口径要诚实**:oplog 里的历史署名、已交付产物、任务轨迹**不删**(内核纯净红线)。
242463
+ * 「注销」= 这个人再也登不进来、不再是任何 active 组织的成员,不等于「个人数据被永久删除」。
242464
+ */
242465
+ async finalizeAccountDeletion(accountId) {
242466
+ const account = await this.store.getAccount(accountId);
242467
+ if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
242468
+ await this.store.upsertAccount({ ...account, status: "disabled" });
242469
+ await this.store.bumpAccountTokenVersion(accountId);
241832
242470
  }
241833
242471
  // —— 邀请(本地模型,CO-005)——
241834
242472
  /**
@@ -242263,33 +242901,32 @@ var init_service6 = __esm({
242263
242901
  if (!c) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
242264
242902
  return c;
242265
242903
  }
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
242904
  };
242271
242905
  }
242272
242906
  });
242273
242907
 
242274
242908
  // ../server/src/domains/companies/offboarding.ts
242275
- var import_node_crypto59, MemberOffboardingService;
242909
+ var import_node_crypto59, OWNER_PROTECTED_MESSAGE, MemberOffboardingService;
242276
242910
  var init_offboarding = __esm({
242277
242911
  "../server/src/domains/companies/offboarding.ts"() {
242278
242912
  "use strict";
242279
242913
  import_node_crypto59 = require("node:crypto");
242280
242914
  init_service6();
242915
+ 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
242916
  MemberOffboardingService = class {
242282
242917
  store;
242283
242918
  now;
242284
242919
  listInFlight;
242285
242920
  transferAssignment;
242286
242921
  disableActor;
242922
+ restoreActor;
242287
242923
  constructor(opts) {
242288
242924
  this.store = opts.store;
242289
242925
  this.now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
242290
242926
  this.listInFlight = opts.listInFlightAssignments;
242291
242927
  this.transferAssignment = opts.transferAssignment;
242292
242928
  this.disableActor = opts.disableActor;
242929
+ this.restoreActor = opts.restoreActor;
242293
242930
  }
242294
242931
  /** 该成员是不是公司最后一个 owner(最后一个 owner 立即阻断,先于任何交接讨论)。 */
242295
242932
  async isLastOwner(companyId, accountId) {
@@ -242315,12 +242952,13 @@ var init_offboarding = __esm({
242315
242952
  */
242316
242953
  async blockersForRemoval(companyId, accountId) {
242317
242954
  const blockers = [];
242318
- const member = await this.store.getMember(companyId, accountId);
242955
+ const members = await this.store.listMembers(companyId);
242956
+ const member = members.find((m2) => m2.accountId === accountId);
242319
242957
  if (!member) {
242320
242958
  return [{ code: "NOT_FOUND", message: `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}` }];
242321
242959
  }
242322
- if (await this.isLastOwner(companyId, accountId)) {
242323
- 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" });
242960
+ if (member.role === "owner") {
242961
+ blockers.push({ code: "OWNER_PROTECTED", message: OWNER_PROTECTED_MESSAGE });
242324
242962
  }
242325
242963
  return blockers;
242326
242964
  }
@@ -242342,6 +242980,7 @@ var init_offboarding = __esm({
242342
242980
  async plan(companyId, accountId, initiatedBy) {
242343
242981
  const member = await this.store.getMember(companyId, accountId);
242344
242982
  if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
242983
+ if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
242345
242984
  if (await this.isLastOwner(companyId, accountId)) {
242346
242985
  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
242986
  }
@@ -242388,10 +243027,9 @@ var init_offboarding = __esm({
242388
243027
  if (op.version !== expectedVersion) {
242389
243028
  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
243029
  }
242391
- if (await this.isLastOwner(companyId, accountId)) {
242392
- 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
- }
242394
- const released = await this.releaseOwnedAgents(companyId, accountId);
243030
+ const members = await this.store.listMembers(companyId);
243031
+ const target = members.find((m2) => m2.accountId === accountId);
243032
+ if (target?.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
242395
243033
  const remaining = await this.blockersForRemoval(companyId, accountId);
242396
243034
  if (remaining.length > 0) {
242397
243035
  await this.store.updateMemberOffboardingIfVersion(
@@ -242403,19 +243041,7 @@ var init_offboarding = __esm({
242403
243041
  );
242404
243042
  throw new CompanyError(409, remaining[0].code, `\u4ECD\u6709\u672A\u4EA4\u63A5\u9879\uFF1A${remaining.map((b2) => b2.message).join("; ")}`);
242405
243043
  }
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
- }
243044
+ const released = await this.applyMemberRemoval(companyId, accountId);
242419
243045
  await this.store.updateMemberOffboardingIfVersion(
242420
243046
  companyId,
242421
243047
  operationId,
@@ -242425,6 +243051,65 @@ var init_offboarding = __esm({
242425
243051
  );
242426
243052
  return { status: "removed", transferred: [], left: released };
242427
243053
  }
243054
+ /**
243055
+ * 移除一条成员关系的**全套副作用**,三条移除路径共用(管理员移除他人 `commit`、
243056
+ * 本人退出组织 `DELETE …/members/me`、注销账号 `DELETE /api/accounts/me`)。
243057
+ *
243058
+ * 顺序与原子性(2026-09-23 代码评审打回后重排):
243059
+ * ① 读一遍成员行:不存在 → 404;**已经是组织管理员 → 当场 403,一个副作用都不做**(D5.2)。
243060
+ * ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行)。
243061
+ * 这一步必须在③之前:成员行一删,本公司的 scoped registry 就不再认这个 actor,
243062
+ * 停用会静默 no-op,人被移除了却还挂在花名册上(QA D3 修过一次的病)。
243063
+ * ③ **一个事务**做完控制面三件事:锁内复核目标不是组织管理员 → 清名下 agent 的负责人字段
243064
+ * (`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,不清就是裸 FK 违例;
243065
+ * **只清 owner**,不改状态、不撤凭证、不解绑助理,PRD v1.5)→ 删 human principal bridge
243066
+ * → 删成员行(`store.removeMemberAndReleaseOwnedAgents`)。
243067
+ * ④ ③失败(并发转交把这个人升成了组织管理员,A09)→ 把②CAS 还原回去。
243068
+ *
243069
+ * **为什么③非要是一个事务**:以前①②③是「先清负责人、再停用、再删桥,最后才去拿锁删成员行」
243070
+ * 四段独立的写。A09 那条并发(`O` 转交给 `A1` 的同时另一位管理员删 `A1`)走到最后一步才被
243071
+ * `OWNER_PROTECTED` 拒——而此刻 `A1` 名下 Agent 的负责人已经被清空、桥已经被删、人已经被停用。
243072
+ * 请求失败了,破坏却留下了:新上任的组织管理员丢了名下全部 Agent 的归属、还从花名册上消失。
243073
+ * 现在控制面那三件事共一把锁(与转交锁同一组行),失败时一行都没写;跨不进这把锁的只有员工侧
243074
+ * 停用,用 CAS 补偿还原——「停用之后有别人写过这个 actor」就跳过,不撤销他人的合法写(QA R9)。
243075
+ */
243076
+ async applyMemberRemoval(companyId, accountId) {
243077
+ const member = await this.store.getMember(companyId, accountId);
243078
+ if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243079
+ if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
243080
+ let ticket = null;
243081
+ if (!await isEmployedElsewhere(this.store, companyId, accountId)) {
243082
+ ticket = await this.disableActor?.(companyId, member.actorId) ?? null;
243083
+ }
243084
+ const removed = await this.store.removeMemberAndReleaseOwnedAgents(companyId, accountId, this.now()).catch(async (e) => {
243085
+ await this.compensateDisable(companyId, ticket);
243086
+ throw e;
243087
+ });
243088
+ if (!removed.ok) {
243089
+ await this.compensateDisable(companyId, ticket);
243090
+ if (removed.reason === "not_found") {
243091
+ throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
243092
+ }
243093
+ throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
243094
+ }
243095
+ return removed.releasedAgents;
243096
+ }
243097
+ /**
243098
+ * 把②那次停用还原回去。**只还原本次自己造成的那一次**:`restoreActor` 走 CAS,
243099
+ * 停用之后但凡有别人写过这个 actor 就跳过——留下「成员还在 + actor 已停用」这个合法终态
243100
+ * (与员工页 `DELETE /api/actors/:id` 的终态同构),比静默撤销他人的合法停用安全。
243101
+ * 补偿本身失败不改写主错误:调用方要看到的是「为什么没删成」,不是补偿的二级故障。
243102
+ */
243103
+ async compensateDisable(companyId, ticket) {
243104
+ if (!ticket || !this.restoreActor) return;
243105
+ try {
243106
+ await this.restoreActor(companyId, ticket);
243107
+ } catch (e) {
243108
+ console.error(
243109
+ `[offboarding] restore actor ${ticket.actorId} failed after removal was rejected: ${e instanceof Error ? e.message : String(e)}`
243110
+ );
243111
+ }
243112
+ }
242428
243113
  /**
242429
243114
  * 把该成员名下所有 agent 的负责人字段清空,返回被清的 agent id。
242430
243115
  *
@@ -242512,13 +243197,28 @@ function companiesDomain(opts) {
242512
243197
  }
242513
243198
  return n;
242514
243199
  };
243200
+ const requireActiveCompany = async (companyId) => {
243201
+ const company = await service.get(companyId);
243202
+ if (!company || company.status !== "active") {
243203
+ throw new ApiError(404, "COMPANY_NOT_FOUND", `\u7EC4\u7EC7\u4E0D\u5B58\u5728\u6216\u5DF2\u4E0D\u53EF\u7528\uFF1A${companyId}`);
243204
+ }
243205
+ };
242515
243206
  const requireOwnerOrAdmin = async (req, companyId) => {
242516
243207
  const me = await whoami(req);
242517
243208
  const member = await service.getMember(companyId, me.accountId);
242518
- if (!member || member.role !== "owner" && member.role !== "admin") {
243209
+ if (!member) throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
243210
+ await requireActiveCompany(companyId);
243211
+ if (member.role !== "owner" && member.role !== "admin") {
242519
243212
  throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
242520
243213
  }
242521
243214
  };
243215
+ const requireMember = async (req, companyId) => {
243216
+ const me = await whoami(req);
243217
+ const member = await service.getMember(companyId, me.accountId);
243218
+ if (!member) throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
243219
+ await requireActiveCompany(companyId);
243220
+ return me;
243221
+ };
242522
243222
  return (router) => {
242523
243223
  router.get("/api/companies", async (req) => {
242524
243224
  const me = await whoami(req);
@@ -242578,7 +243278,9 @@ function companiesDomain(opts) {
242578
243278
  return { status: 200, body: { ok: true } };
242579
243279
  });
242580
243280
  router.get("/api/companies/:id/members", async (req) => {
242581
- const items = await service.listMemberViews(req.params.id).catch(mapErr);
243281
+ const cid = req.params.id;
243282
+ await requireMember(req, cid);
243283
+ const items = await service.listMemberViews(cid).catch(mapErr);
242582
243284
  return { status: 200, body: { items } };
242583
243285
  });
242584
243286
  router.get("/api/companies/:id/members/me", async (req) => {
@@ -242608,6 +243310,9 @@ function companiesDomain(opts) {
242608
243310
  if (b2.role === void 0 && b2.displayName === void 0) {
242609
243311
  throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08role / displayName\uFF09");
242610
243312
  }
243313
+ const me = await whoami(req);
243314
+ if (b2.role !== void 0 || me.accountId !== aid) await requireOwnerOrAdmin(req, cid);
243315
+ else await requireMember(req, cid);
242611
243316
  let member;
242612
243317
  if (b2.role !== void 0) {
242613
243318
  if (!b2.role) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 role");
@@ -242622,11 +243327,19 @@ function companiesDomain(opts) {
242622
243327
  }
242623
243328
  return { status: 200, body: member };
242624
243329
  });
243330
+ router.delete("/api/companies/:id/members/me", async (req) => {
243331
+ const off = requireOffboarding();
243332
+ const cid = req.params.id;
243333
+ const me = await whoami(req);
243334
+ await service.peekSelfLeave(cid, me.accountId).catch(mapErr);
243335
+ const released = await off.applyMemberRemoval(cid, me.accountId).catch(mapErr);
243336
+ return { status: 200, body: { ok: true, releasedAgents: released } };
243337
+ });
242625
243338
  router.delete("/api/companies/:id/members/:accountId", async (req) => {
242626
243339
  const cid = req.params.id;
242627
243340
  const aid = req.params.accountId;
243341
+ await requireOwnerOrAdmin(req, cid);
242628
243342
  const member = await service.peekRemovableMember(cid, aid).catch(mapErr);
242629
- if (offboarding) await offboarding.releaseOwnedAgents(cid, aid).catch(mapErr);
242630
243343
  let actorsSvc;
242631
243344
  let ownedDisableBefore = null;
242632
243345
  let disableSeq = 0;
@@ -242666,6 +243379,32 @@ function companiesDomain(opts) {
242666
243379
  }
242667
243380
  return { status: 200, body: { ok: true } };
242668
243381
  });
243382
+ router.post("/api/companies/:id/owner", async (req) => {
243383
+ const cid = req.params.id;
243384
+ const b2 = req.body ?? {};
243385
+ if (typeof b2.toAccountId !== "string" || !b2.toAccountId.trim()) {
243386
+ throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId: string");
243387
+ }
243388
+ const me = await whoami(req);
243389
+ const res = await service.transferOwner(cid, me.accountId, b2.toAccountId.trim()).catch(mapErr);
243390
+ return { status: 200, body: res };
243391
+ });
243392
+ router.delete("/api/companies/:id", async (req) => {
243393
+ const cid = req.params.id;
243394
+ const me = await whoami(req);
243395
+ const company = await service.dissolveCompany(cid, me.accountId).catch(mapErr);
243396
+ return { status: 200, body: company };
243397
+ });
243398
+ router.delete("/api/accounts/me", async (req) => {
243399
+ const off = requireOffboarding();
243400
+ const me = await whoami(req);
243401
+ const memberships = await service.peekAccountDeletion(me.accountId).catch(mapErr);
243402
+ for (const m2 of memberships) {
243403
+ await off.applyMemberRemoval(m2.companyId, me.accountId).catch(mapErr);
243404
+ }
243405
+ await service.finalizeAccountDeletion(me.accountId).catch(mapErr);
243406
+ return { status: 200, body: { ok: true, leftCompanies: memberships.map((m2) => m2.companyId) } };
243407
+ });
242669
243408
  router.get("/api/companies/:id/agent-credentials", async (req) => {
242670
243409
  const creds = requireCredentials();
242671
243410
  const cid = req.params.id;
@@ -242713,6 +243452,7 @@ function companiesDomain(opts) {
242713
243452
  router.post("/api/companies/:id/invitations", async (req) => {
242714
243453
  const b2 = req.body ?? {};
242715
243454
  if (!b2.email) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 email");
243455
+ await requireOwnerOrAdmin(req, req.params.id);
242716
243456
  const role = b2.role ?? "member";
242717
243457
  const me = await whoami(req);
242718
243458
  const invitation = await service.createInvitation(req.params.id, b2.email, role, me.accountId).catch(mapErr);
@@ -242852,7 +243592,7 @@ var init_routes6 = __esm({
242852
243592
  init_router();
242853
243593
  init_service6();
242854
243594
  init_image_upload();
242855
- COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links"];
243595
+ COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links", "/api/accounts"];
242856
243596
  }
242857
243597
  });
242858
243598
 
@@ -242995,9 +243735,30 @@ function createCompaniesDomain(opts) {
242995
243735
  ...opts.resolveActorsCtx !== void 0 ? {
242996
243736
  // 未认领转 left 的 agent、以及 commit 移除掉的那位成员,都要把员工侧 ActorRecord
242997
243737
  // 一并软停用——否则控制面说「已离开/已移除」、花名册还显示在岗,两个事实源当场分叉。
243738
+ //
243739
+ // 用 `disableActorIfCurrentlyActive` 而不是 `disableActor`,并把 `{before, seq}` 回给
243740
+ // offboarding:只有**真的把这个 actor 从 active 拉到 disabled** 的那一次调用才拿得到
243741
+ // 返回值,也才「拥有」这次状态变更;已被并发请求或员工页先停用的一律 ok:false,
243742
+ // 本次就不 own、移除失败也不补偿(QA R10 的判据,两条移除路径同一套)。
242998
243743
  disableActor: async (companyId, actorId) => {
242999
243744
  const ctx = await opts.resolveActorsCtx(companyId);
243000
- await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
243745
+ const res = await ctx.service.disableActorIfCurrentlyActive(actorId, "system:offboarding");
243746
+ return res.ok ? { actorId, before: res.before, seq: res.seq } : null;
243747
+ },
243748
+ // 移除最终失败时的补偿:**CAS** 写回停用前那一行。`expectedSeq` 对不上(停用之后
243749
+ // 有任何第三方写过这个 actor)就跳过——不静默撤销他人的合法停用 / 改名(QA R9)。
243750
+ restoreActor: async (companyId, ticket) => {
243751
+ const ctx = await opts.resolveActorsCtx(companyId);
243752
+ const res = await ctx.service.casUpsertActor(
243753
+ { ...ticket.before, status: "active" },
243754
+ ticket.seq,
243755
+ "system:offboarding"
243756
+ );
243757
+ if (!res.ok) {
243758
+ console.info(
243759
+ `[companies] restore skipped for ${ticket.actorId}: another write intervened after the disable`
243760
+ );
243761
+ }
243001
243762
  }
243002
243763
  } : {}
243003
243764
  });
@@ -259991,7 +260752,35 @@ var init_pool = __esm({
259991
260752
  });
259992
260753
 
259993
260754
  // ../storage/src/postgres-control-plane.ts
259994
- var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
260755
+ async function ensureOneOwnerInvariant(pool, s2) {
260756
+ try {
260757
+ const bad = await pool.query(
260758
+ `SELECT company_id, count(*) FILTER (WHERE role = 'owner') AS owners
260759
+ FROM "${s2}".company_members
260760
+ GROUP BY company_id
260761
+ HAVING count(*) FILTER (WHERE role = 'owner') <> 1
260762
+ ORDER BY company_id`
260763
+ );
260764
+ if (bad.rowCount && bad.rowCount > 0) {
260765
+ console.warn(
260766
+ `[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`
260767
+ );
260768
+ }
260769
+ } catch (e) {
260770
+ 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)}`);
260771
+ }
260772
+ try {
260773
+ await pool.query(
260774
+ `CREATE UNIQUE INDEX IF NOT EXISTS company_members_one_owner_idx
260775
+ ON "${s2}".company_members (company_id) WHERE role = 'owner'`
260776
+ );
260777
+ } catch (e) {
260778
+ console.warn(
260779
+ `[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)}`
260780
+ );
260781
+ }
260782
+ }
260783
+ var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, migrationRow2, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
259995
260784
  var init_postgres_control_plane = __esm({
259996
260785
  "../storage/src/postgres-control-plane.ts"() {
259997
260786
  "use strict";
@@ -260021,6 +260810,8 @@ var init_postgres_control_plane = __esm({
260021
260810
  onboarding_variant text -- \u767B\u5F55\u540E\u52A9\u7406\u5F15\u5BFC\u53D8\u4F53 A|B\uFF1BNULL=\u7F3A\u7701\u89C6\u4E3A A
260022
260811
  )`);
260023
260812
  await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS onboarding_variant text`);
260813
+ await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_at timestamptz`);
260814
+ await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_by text`);
260024
260815
  await pool.query(`
260025
260816
  CREATE TABLE IF NOT EXISTS "${s2}".company_members (
260026
260817
  company_id text NOT NULL,
@@ -260209,6 +261000,19 @@ var init_postgres_control_plane = __esm({
260209
261000
  CHECK (status IN ('handoff_required','handoff_in_progress','ready_to_remove','removed'))
260210
261001
  )`);
260211
261002
  await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS member_offboardings_active_uq ON "${s2}".member_offboardings (company_id, account_id) WHERE status <> 'removed'`);
261003
+ await pool.query(`
261004
+ CREATE TABLE IF NOT EXISTS "${s2}".company_member_role_migrations (
261005
+ id bigserial PRIMARY KEY,
261006
+ company_id text NOT NULL,
261007
+ account_id text NOT NULL,
261008
+ role_before text NOT NULL,
261009
+ role_after text NOT NULL,
261010
+ evidence text NOT NULL, -- 'E1' | 'manifest'
261011
+ manifest_version text,
261012
+ applied_at timestamptz NOT NULL DEFAULT now()
261013
+ )`);
261014
+ 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)`);
261015
+ await ensureOneOwnerInvariant(pool, s2);
260212
261016
  return new _PostgresControlPlaneStore(pool, schema);
260213
261017
  }
260214
261018
  // —— 公司 ——
@@ -260249,11 +261053,51 @@ var init_postgres_control_plane = __esm({
260249
261053
  args.push(patch.onboardingVariant);
260250
261054
  sets.push(`onboarding_variant = $${args.length}`);
260251
261055
  }
261056
+ if (patch.dissolvedAt !== void 0) {
261057
+ args.push(patch.dissolvedAt);
261058
+ sets.push(`dissolved_at = $${args.length}`);
261059
+ }
261060
+ if (patch.dissolvedBy !== void 0) {
261061
+ args.push(patch.dissolvedBy);
261062
+ sets.push(`dissolved_by = $${args.length}`);
261063
+ }
260252
261064
  args.push(id);
260253
261065
  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
261066
  const r = await this.pool.query(sql, args);
260255
261067
  if (r.rowCount === 0) throw new Error(`company not found: ${id}`);
260256
261068
  }
261069
+ async dissolveCompanyIfOwner(companyId, accountId, at2) {
261070
+ const client = await this.pool.connect();
261071
+ try {
261072
+ await client.query("BEGIN");
261073
+ const caller = await client.query(
261074
+ `SELECT role FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2 FOR UPDATE`,
261075
+ [companyId, accountId]
261076
+ );
261077
+ if (!caller.rows[0] || caller.rows[0].role !== "owner") {
261078
+ await client.query("ROLLBACK");
261079
+ const exists = await this.pool.query(`SELECT 1 FROM ${this.s}.companies WHERE id = $1`, [companyId]);
261080
+ return { ok: false, reason: exists.rowCount ? "not_owner" : "not_found" };
261081
+ }
261082
+ const updated = await client.query(
261083
+ `UPDATE ${this.s}.companies SET status = 'suspended', dissolved_at = $2, dissolved_by = $3
261084
+ WHERE id = $1 RETURNING *`,
261085
+ [companyId, at2, accountId]
261086
+ );
261087
+ if (!updated.rows[0]) {
261088
+ await client.query("ROLLBACK");
261089
+ return { ok: false, reason: "not_found" };
261090
+ }
261091
+ await client.query("COMMIT");
261092
+ return { ok: true, company: rowToCompany(updated.rows[0]) };
261093
+ } catch (e) {
261094
+ await client.query("ROLLBACK").catch(() => {
261095
+ });
261096
+ throw e;
261097
+ } finally {
261098
+ client.release();
261099
+ }
261100
+ }
260257
261101
  // —— 成员 ——
260258
261102
  async upsertMember(m2) {
260259
261103
  await this.pool.query(
@@ -260327,6 +261171,48 @@ var init_postgres_control_plane = __esm({
260327
261171
  client.release();
260328
261172
  }
260329
261173
  }
261174
+ async removeMemberAndReleaseOwnedAgents(companyId, accountId, at2) {
261175
+ const client = await this.pool.connect();
261176
+ try {
261177
+ await client.query("BEGIN");
261178
+ const all2 = await client.query(
261179
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261180
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261181
+ [companyId]
261182
+ );
261183
+ const target = all2.rows.map(rowToMember).find((m2) => m2.accountId === accountId);
261184
+ if (!target) {
261185
+ await client.query("ROLLBACK");
261186
+ return { ok: false, reason: "not_found" };
261187
+ }
261188
+ if (target.role === "owner") {
261189
+ await client.query("ROLLBACK");
261190
+ return { ok: false, reason: "owner_protected" };
261191
+ }
261192
+ const released = await client.query(
261193
+ `UPDATE ${this.s}.digital_employees SET owner_account_id = NULL, updated_at = $3
261194
+ WHERE company_id = $1 AND owner_account_id = $2
261195
+ RETURNING id`,
261196
+ [companyId, accountId, at2]
261197
+ );
261198
+ await client.query(
261199
+ `DELETE FROM ${this.s}.principal_bridge WHERE company_id = $1 AND principal_type = 'human' AND principal_id = $2`,
261200
+ [companyId, accountId]
261201
+ );
261202
+ await client.query(
261203
+ `DELETE FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2`,
261204
+ [companyId, accountId]
261205
+ );
261206
+ await client.query("COMMIT");
261207
+ return { ok: true, member: target, releasedAgents: released.rows.map((r) => r.id) };
261208
+ } catch (e) {
261209
+ await client.query("ROLLBACK").catch(() => {
261210
+ });
261211
+ throw e;
261212
+ } finally {
261213
+ client.release();
261214
+ }
261215
+ }
260330
261216
  async updateMemberRoleIfNotLastOwner(companyId, accountId, role) {
260331
261217
  const client = await this.pool.connect();
260332
261218
  try {
@@ -260365,6 +261251,170 @@ var init_postgres_control_plane = __esm({
260365
261251
  client.release();
260366
261252
  }
260367
261253
  }
261254
+ async setSoleCompanyOwner(companyId, toAccountId, opts) {
261255
+ const client = await this.pool.connect();
261256
+ try {
261257
+ await client.query("BEGIN");
261258
+ const all2 = await client.query(
261259
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261260
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261261
+ [companyId]
261262
+ );
261263
+ const rows = all2.rows.map(rowToMember);
261264
+ const target = rows.find((m2) => m2.accountId === toAccountId);
261265
+ if (!target) {
261266
+ await client.query("ROLLBACK");
261267
+ return { ok: false, reason: "not_found" };
261268
+ }
261269
+ if (opts?.requireCurrentOwner !== void 0) {
261270
+ const caller = rows.find((m2) => m2.accountId === opts.requireCurrentOwner);
261271
+ if (!caller || caller.role !== "owner") {
261272
+ await client.query("ROLLBACK");
261273
+ return { ok: false, reason: "not_owner" };
261274
+ }
261275
+ }
261276
+ if (!target.onboardingCompletedAt && target.role !== "owner") {
261277
+ await client.query("ROLLBACK");
261278
+ return { ok: false, reason: "target_not_listed" };
261279
+ }
261280
+ const acc = await client.query(
261281
+ `SELECT status FROM ${this.s}.accounts WHERE id = $1`,
261282
+ [toAccountId]
261283
+ );
261284
+ if (!acc.rows[0] || acc.rows[0].status !== "active") {
261285
+ await client.query("ROLLBACK");
261286
+ return { ok: false, reason: "target_inactive" };
261287
+ }
261288
+ const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
261289
+ if (others.length === 0 && target.role === "owner") {
261290
+ await client.query("COMMIT");
261291
+ return { ok: true, owner: target, demoted: [] };
261292
+ }
261293
+ if (others.length > 0) {
261294
+ await client.query(
261295
+ `UPDATE ${this.s}.company_members SET role = 'admin', updated_at = now()
261296
+ WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
261297
+ [companyId, toAccountId]
261298
+ );
261299
+ }
261300
+ if (target.role !== "owner") {
261301
+ await client.query(
261302
+ `UPDATE ${this.s}.company_members SET role = 'owner', updated_at = now()
261303
+ WHERE company_id = $1 AND account_id = $2`,
261304
+ [companyId, toAccountId]
261305
+ );
261306
+ }
261307
+ await client.query("COMMIT");
261308
+ return {
261309
+ ok: true,
261310
+ owner: { ...target, role: "owner" },
261311
+ demoted: others.map((m2) => ({ ...m2, role: "admin" }))
261312
+ };
261313
+ } catch (e) {
261314
+ await client.query("ROLLBACK").catch(() => {
261315
+ });
261316
+ throw e;
261317
+ } finally {
261318
+ client.release();
261319
+ }
261320
+ }
261321
+ // —— 存量身份映射(ADR D3)——
261322
+ async migrateSoleCompanyOwner(companyId, toAccountId, audit2) {
261323
+ const client = await this.pool.connect();
261324
+ try {
261325
+ await client.query("BEGIN");
261326
+ const all2 = await client.query(
261327
+ `SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
261328
+ FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
261329
+ [companyId]
261330
+ );
261331
+ const rows = all2.rows.map(rowToMember);
261332
+ const target = rows.find((m2) => m2.accountId === toAccountId);
261333
+ if (!target) {
261334
+ await client.query("ROLLBACK");
261335
+ return { ok: false, reason: "not_found" };
261336
+ }
261337
+ const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
261338
+ if (others.length === 0 && target.role === "owner") {
261339
+ await client.query("COMMIT");
261340
+ return { ok: true, owner: target, demoted: [], auditRows: [] };
261341
+ }
261342
+ if (others.length > 0) {
261343
+ await client.query(
261344
+ `UPDATE ${this.s}.company_members SET role = 'admin' WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
261345
+ [companyId, toAccountId]
261346
+ );
261347
+ }
261348
+ if (target.role !== "owner") {
261349
+ await client.query(
261350
+ `UPDATE ${this.s}.company_members SET role = 'owner' WHERE company_id = $1 AND account_id = $2`,
261351
+ [companyId, toAccountId]
261352
+ );
261353
+ }
261354
+ const auditRows = [
261355
+ ...others.map((m2) => migrationRow2(companyId, m2.accountId, "owner", "admin", audit2)),
261356
+ ...target.role !== "owner" ? [migrationRow2(companyId, toAccountId, target.role, "owner", audit2)] : []
261357
+ ];
261358
+ for (const r of auditRows) {
261359
+ await client.query(
261360
+ `INSERT INTO ${this.s}.company_member_role_migrations
261361
+ (company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
261362
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
261363
+ [r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
261364
+ );
261365
+ }
261366
+ await client.query("COMMIT");
261367
+ return {
261368
+ ok: true,
261369
+ owner: { ...target, role: "owner" },
261370
+ demoted: others.map((m2) => ({ ...m2, role: "admin" })),
261371
+ auditRows
261372
+ };
261373
+ } catch (e) {
261374
+ await client.query("ROLLBACK").catch(() => {
261375
+ });
261376
+ throw e;
261377
+ } finally {
261378
+ client.release();
261379
+ }
261380
+ }
261381
+ async setMemberRoleOnly(companyId, accountId, role) {
261382
+ const r = await this.pool.query(
261383
+ `UPDATE ${this.s}.company_members SET role = $3 WHERE company_id = $1 AND account_id = $2`,
261384
+ [companyId, accountId, role]
261385
+ );
261386
+ return (r.rowCount ?? 0) > 0;
261387
+ }
261388
+ // —— 存量身份映射审计(ADR D3.4)——
261389
+ async appendMemberRoleMigrations(rows) {
261390
+ if (rows.length === 0) return;
261391
+ const client = await this.pool.connect();
261392
+ try {
261393
+ await client.query("BEGIN");
261394
+ for (const r of rows) {
261395
+ await client.query(
261396
+ `INSERT INTO ${this.s}.company_member_role_migrations
261397
+ (company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
261398
+ VALUES ($1,$2,$3,$4,$5,$6,$7)`,
261399
+ [r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
261400
+ );
261401
+ }
261402
+ await client.query("COMMIT");
261403
+ } catch (e) {
261404
+ await client.query("ROLLBACK").catch(() => {
261405
+ });
261406
+ throw e;
261407
+ } finally {
261408
+ client.release();
261409
+ }
261410
+ }
261411
+ async listMemberRoleMigrations(companyId) {
261412
+ 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(
261413
+ `SELECT * FROM ${this.s}.company_member_role_migrations WHERE company_id = $1 ORDER BY applied_at, id`,
261414
+ [companyId]
261415
+ );
261416
+ return r.rows.map(rowToRoleMigration);
261417
+ }
260368
261418
  // —— 邀请 ——
260369
261419
  async createInvitation(inv) {
260370
261420
  await this.pool.query(
@@ -260834,7 +261884,28 @@ var init_postgres_control_plane = __esm({
260834
261884
  ...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
260835
261885
  status: row.status,
260836
261886
  createdAt: new Date(row.created_at).toISOString(),
260837
- ...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {}
261887
+ ...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {},
261888
+ // 解散痕迹(组织三层身份 ADR D9.2):两列一起有、一起空。普通运维停用不写它们。
261889
+ ...row.dissolved_at != null ? { dissolvedAt: toIso(row.dissolved_at) } : {},
261890
+ ...row.dissolved_by != null ? { dissolvedBy: row.dissolved_by } : {}
261891
+ });
261892
+ rowToRoleMigration = (row) => ({
261893
+ companyId: row.company_id,
261894
+ accountId: row.account_id,
261895
+ roleBefore: row.role_before,
261896
+ roleAfter: row.role_after,
261897
+ evidence: row.evidence,
261898
+ ...row.manifest_version != null ? { manifestVersion: row.manifest_version } : {},
261899
+ appliedAt: toIso(row.applied_at)
261900
+ });
261901
+ migrationRow2 = (companyId, accountId, roleBefore, roleAfter, audit2) => ({
261902
+ companyId,
261903
+ accountId,
261904
+ roleBefore,
261905
+ roleAfter,
261906
+ evidence: audit2.evidence,
261907
+ ...audit2.evidence === "manifest" && audit2.manifestVersion !== void 0 ? { manifestVersion: audit2.manifestVersion } : {},
261908
+ appliedAt: audit2.appliedAt
260838
261909
  });
260839
261910
  rowToMember = (row) => ({
260840
261911
  companyId: row.company_id,
@@ -276559,6 +277630,7 @@ __export(src_exports2, {
276559
277630
  ACTOR_DELETED_CHAT_MESSAGE: () => ACTOR_DELETED_CHAT_MESSAGE,
276560
277631
  ACTOR_DELETED_ERROR_CODE: () => ACTOR_DELETED_ERROR_CODE,
276561
277632
  ASSISTANT_ARCHIVED_CODE: () => ASSISTANT_ARCHIVED_CODE,
277633
+ ASSISTANT_POSITION: () => ASSISTANT_POSITION,
276562
277634
  ASSISTANT_TIER_RULES: () => ASSISTANT_TIER_RULES,
276563
277635
  AUTOMATION_TEMPLATES: () => AUTOMATION_TEMPLATES,
276564
277636
  AUTONOMOUS_ETHOS: () => AUTONOMOUS_ETHOS,
@@ -276566,6 +277638,7 @@ __export(src_exports2, {
276566
277638
  ActorMemoryService: () => ActorMemoryService,
276567
277639
  ActorsService: () => ActorsService,
276568
277640
  AgentCredentialService: () => AgentCredentialService,
277641
+ AgentManageForbiddenError: () => AgentManageForbiddenError,
276569
277642
  ApiError: () => ApiError,
276570
277643
  AssistantLazyCreateError: () => AssistantLazyCreateError,
276571
277644
  AssistantsService: () => AssistantsService,
@@ -276703,6 +277776,7 @@ __export(src_exports2, {
276703
277776
  OPERATOR_SCOPE: () => OPERATOR_SCOPE,
276704
277777
  ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
276705
277778
  OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
277779
+ OWNER_PROTECTED_MESSAGE: () => OWNER_PROTECTED_MESSAGE,
276706
277780
  PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
276707
277781
  PARENT_CONVERSATION_RECENT_TURNS: () => PARENT_CONVERSATION_RECENT_TURNS,
276708
277782
  PostgresNotificationStore: () => PostgresNotificationStore,
@@ -276757,11 +277831,16 @@ __export(src_exports2, {
276757
277831
  applyWorkspaceBundle: () => applyWorkspaceBundle,
276758
277832
  assembleDelegationsDomain: () => assembleDelegationsDomain,
276759
277833
  assertBindingWritePermission: () => assertBindingWritePermission,
277834
+ assertCompanyManager: () => assertCompanyManager,
276760
277835
  assertCreatableVariable: () => assertCreatableVariable,
277836
+ assertMayCreateAgent: () => assertMayCreateAgent,
277837
+ assertMayManageAgent: () => assertMayManageAgent,
277838
+ assertMaySelfOrManage: () => assertMaySelfOrManage,
276761
277839
  assistantRowRecovery: () => assistantRowRecovery,
276762
277840
  assistantsDomain: () => assistantsDomain,
276763
277841
  atCapacity: () => atCapacity,
276764
277842
  automationsDomain: () => automationsDomain,
277843
+ backfillOrgIdentity: () => backfillOrgIdentity,
276765
277844
  backfillOrgModel: () => backfillOrgModel,
276766
277845
  backfillOrgModelOnStartup: () => backfillOrgModelOnStartup,
276767
277846
  backfillResolvedLetterReadMarkers: () => backfillResolvedLetterReadMarkers,
@@ -276934,8 +278013,11 @@ __export(src_exports2, {
276934
278013
  hideDeleted: () => hideDeleted,
276935
278014
  humanSubmitSourceRunId: () => humanSubmitSourceRunId,
276936
278015
  idlenessOf: () => idlenessOf,
278016
+ inventoryOrgIdentity: () => inventoryOrgIdentity,
276937
278017
  isActiveCoordinatorAgent: () => isActiveCoordinatorAgent,
278018
+ isAssistantAgent: () => isAssistantAgent,
276938
278019
  isCompanyCrossTenantPath: () => isCompanyCrossTenantPath,
278020
+ isCompanyManager: () => isCompanyManager,
276939
278021
  isCoordinatorAgentName: () => isCoordinatorAgentName,
276940
278022
  isCoordinatorRunReader: () => isCoordinatorRunReader,
276941
278023
  isCredentialAuditKind: () => isCredentialAuditKind,
@@ -276995,6 +278077,7 @@ __export(src_exports2, {
276995
278077
  matchedKnowledgeQueryTerms: () => matchedKnowledgeQueryTerms,
276996
278078
  materializeBuiltinSkills: () => materializeBuiltinSkills,
276997
278079
  materializeSkillFiles: () => materializeSkillFiles,
278080
+ mayManageAgent: () => mayManageAgent,
276998
278081
  mayManageWorkorder: () => mayManageWorkorder,
276999
278082
  mayReassignProjectItem: () => mayReassignProjectItem,
277000
278083
  memberNameResolver: () => memberNameResolver,
@@ -277109,6 +278192,7 @@ __export(src_exports2, {
277109
278192
  retireCdGateInTypeDef: () => retireCdGateInTypeDef,
277110
278193
  retireCdGateInTypeStore: () => retireCdGateInTypeStore,
277111
278194
  rolesDomain: () => rolesDomain,
278195
+ rollbackOrgIdentity: () => rollbackOrgIdentity,
277112
278196
  routeFusedSignal: () => routeFusedSignal,
277113
278197
  runtimeConnectionStatus: () => runtimeConnectionStatus,
277114
278198
  runtimeDriftBlock: () => runtimeDriftBlock,
@@ -277200,6 +278284,7 @@ var init_src11 = __esm({
277200
278284
  init_outputs_scan();
277201
278285
  init_outputs_copy();
277202
278286
  init_org_model_backfill();
278287
+ init_org_identity_migration();
277203
278288
  init_types_admin();
277204
278289
  init_roles_admin();
277205
278290
  init_roles_seed();
@@ -277311,6 +278396,7 @@ var init_src11 = __esm({
277311
278396
  init_chat_builtin_skills();
277312
278397
  init_connector_skills();
277313
278398
  init_connector_tool_versions();
278399
+ init_agent_manage_guard();
277314
278400
  init_board_watch();
277315
278401
  init_uncategorized_project();
277316
278402
  init_run_settlement();
@@ -280989,6 +282075,28 @@ async function startServe(opts) {
280989
282075
  const role = members.find((m2) => m2.actorId === actorId)?.role;
280990
282076
  return role === "owner" || role === "admin" || role === "member" ? role : null;
280991
282077
  },
282078
+ /* Agent 写面的行政权闸(组织三层身份 ADR D6,判据在 `governance/agent-manage-guard.ts`)。
282079
+ 三条读取面都**每请求现查**、都按 `(组织, 账号)` 双键——禁止把角色塞进任何跨请求缓存
282080
+ (D7.1:`agentPrincipalByToken` 只缓存 `{companyId, actorId}`,不许顺手加 role)。
282081
+ `controlPlaneStore` / `getEngine` / `resolveAssistantsCtx` 在下方才就绪;这三个箭头只在
282082
+ 请求时跑(同 `projectExists` 的惰性模式)。 */
282083
+ agentManageGuard: {
282084
+ callerRole: async (companyId, callerActorId) => {
282085
+ const cid = companyId ?? defaultCompanyId;
282086
+ const members = await controlPlaneStore.listMembers(cid);
282087
+ const row = members.find((m2) => m2.actorId === callerActorId || m2.accountId === callerActorId);
282088
+ return row?.role;
282089
+ },
282090
+ boundHuman: async (companyId, agentId) => {
282091
+ const ctx = await resolveAssistantsCtxLazy(companyId);
282092
+ if (!ctx) return null;
282093
+ return (await ctx.service.getByAgent(agentId))?.humanActorId ?? null;
282094
+ },
282095
+ reportsTo: async (companyId, humanActorId) => {
282096
+ const engine2 = await getEngine(companyId ?? defaultCompanyId);
282097
+ return (await engine2.registry.getActor(humanActorId))?.reportsTo ?? null;
282098
+ }
282099
+ },
280992
282100
  // CO-302 数据面隔离(actors 域试点):按当前公司取其引擎,用各自 registry 服务该域请求。
280993
282101
  // engineRouter 在下方(行 ~411)以 const 声明;此箭头只在请求时调用,那时已初始化,闭包引用合法
280994
282102
  // (TDZ 只在构造时访问才报错,这里不访问)。默认公司命中现有单实例(registry===registryStore),
@@ -281051,6 +282159,12 @@ async function startServe(opts) {
281051
282159
  isPlatformAdmin: async () => false
281052
282160
  // 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
281053
282161
  });
282162
+ let resolveAssistantsCtxImpl = null;
282163
+ const resolveAssistantsCtx = async (companyId) => {
282164
+ if (!resolveAssistantsCtxImpl) throw new Error("assistants ctx resolver \u5C1A\u672A\u88C5\u914D\uFF08\u542F\u52A8\u671F\u4E0D\u5E94\u6709\u8BF7\u6C42\uFF09");
282165
+ return resolveAssistantsCtxImpl(companyId);
282166
+ };
282167
+ const resolveAssistantsCtxLazy = async (companyId) => resolveAssistantsCtxImpl ? resolveAssistantsCtxImpl(companyId) : null;
281054
282168
  let projectStateStore;
281055
282169
  let artifactStateStore;
281056
282170
  let chatSessionStore;
@@ -281780,8 +282894,7 @@ async function startServe(opts) {
281780
282894
  const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, sharedSchema) : await FileControlPlaneStore.open(path30.join(opts.dir, "control-plane.json"));
281781
282895
  assistantsService.setPlatformAdminChecker(async (caller) => {
281782
282896
  const member = await controlPlaneStore.getMember(defaultCompanyId, caller);
281783
- if (!member) return false;
281784
- return member.role === "owner";
282897
+ return member?.role === "owner" || member?.role === "admin";
281785
282898
  });
281786
282899
  if (pgDsn && pgPool) {
281787
282900
  const priceStore = await PostgresModelPriceStore.open(pgPool, sharedSchema);
@@ -281879,16 +282992,29 @@ async function startServe(opts) {
281879
282992
  }
281880
282993
  }
281881
282994
  if (!await controlPlaneStore.getMember(defaultCompanyId, operatorActor)) {
281882
- await controlPlaneStore.upsertMember({ companyId: defaultCompanyId, accountId: operatorActor, actorId: operatorActor, role: "owner", onboardingCompletedAt: seededAt });
282995
+ const hasOwner = (await controlPlaneStore.listMembers(defaultCompanyId)).some((m2) => m2.role === "owner");
282996
+ await controlPlaneStore.upsertMember({
282997
+ companyId: defaultCompanyId,
282998
+ accountId: operatorActor,
282999
+ actorId: operatorActor,
283000
+ role: hasOwner ? "admin" : "owner",
283001
+ onboardingCompletedAt: seededAt
283002
+ });
283003
+ if (hasOwner) {
283004
+ 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`);
283005
+ }
281883
283006
  }
281884
283007
  }
281885
283008
  const bootstrapEmail = process.env["OASIS_BOOTSTRAP_OWNER_EMAIL"];
281886
283009
  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`);
283010
+ const res = await companies.service.grantOwner(defaultCompanyId, bootstrapEmail);
283011
+ const bootstrapAccount = await controlPlaneStore.getAccountByEmail(bootstrapEmail.trim().toLowerCase());
283012
+ console.log(
283013
+ 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`
283014
+ );
281889
283015
  const bootstrapPw = process.env["OASIS_BOOTSTRAP_OWNER_PASSWORD"];
281890
- if (bootstrapPw) {
281891
- await controlPlaneStore.setAccountPassword(m2.accountId, hashPassword(bootstrapPw));
283016
+ if (bootstrapPw && bootstrapAccount) {
283017
+ await controlPlaneStore.setAccountPassword(bootstrapAccount.id, hashPassword(bootstrapPw));
281892
283018
  console.log(`[serve] bootstrap owner \u5BC6\u7801\u5DF2\u8BBE\uFF08OASIS_BOOTSTRAP_OWNER_PASSWORD\uFF09`);
281893
283019
  }
281894
283020
  }
@@ -282060,6 +283186,36 @@ async function startServe(opts) {
282060
283186
  planeInflight.set(companyId, building);
282061
283187
  return building;
282062
283188
  };
283189
+ resolveAssistantsCtxImpl = async (companyId) => {
283190
+ const cid = companyId ?? defaultCompanyId;
283191
+ const ctx = await actors.resolveCtx(cid);
283192
+ const engine2 = await getEngine(cid);
283193
+ const primary = Boolean(
283194
+ pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
283195
+ );
283196
+ const plane = primary ? await openCompanyPlane(cid) : void 0;
283197
+ return {
283198
+ service: new AssistantsService({
283199
+ bindStore: plane?.assistants ?? assistantBindStore,
283200
+ prefsStore: plane?.humanPrefs ?? humanPrefsStore,
283201
+ registry: engine2.registry,
283202
+ actors: ctx.service,
283203
+ // 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
283204
+ companyId: () => cid,
283205
+ listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
283206
+ // owner | admin 同档(组织三层身份 ADR D6.4):只认 owner 会让「管理员」失去今天
283207
+ // 「组织管理者」本就有的助理管理能力——那是本次改动造成的能力回归,不是新规则。
283208
+ isPlatformAdmin: async (caller) => {
283209
+ const member = await controlPlaneStore.getMember(cid, caller);
283210
+ return member?.role === "owner" || member?.role === "admin";
283211
+ },
283212
+ // 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
283213
+ // (新模型下它恰好返回一个元素,`ownerAssistantRuntime` 的「取第一个有着落的」语义不变)。
283214
+ listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
283215
+ }),
283216
+ getActor: (id) => engine2.registry.getActor(id)
283217
+ };
283218
+ };
282063
283219
  const openCompanyPlaneUncached = async (companyId) => {
282064
283220
  if (!pgPool || !locationStore || !cutoverStore) {
282065
283221
  throw new Error("openCompanyPlane \u9700\u8981 Postgres");
@@ -283121,34 +284277,10 @@ async function startServe(opts) {
283121
284277
  assistantsDomain({
283122
284278
  service: assistantsService,
283123
284279
  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
- }
284280
+ // 与 actors 域的 agentManageGuard.boundHuman **同一份**:「这个 agent 绑给了哪个真人」
284281
+ // 两处各解析一次助理 store 必然漂,而漂出来的差异正好是「删助理」与「解绑助理」两条路
284282
+ // 的闸又对不上(G4 的形状)。
284283
+ resolveService: resolveAssistantsCtx
283152
284284
  }),
283153
284285
  companies.register,
283154
284286
  projectsDomain2.register,
@@ -292604,7 +293736,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
292604
293736
  }
292605
293737
  }
292606
293738
  }
292607
- var CLI_VERSION = "2.2.80" ? "2.2.80" : "dev";
293739
+ var CLI_VERSION = "2.2.82" ? "2.2.82" : "dev";
292608
293740
  function apiHeaders(token, company) {
292609
293741
  const headers = {
292610
293742
  authorization: `Bearer ${token}`,
@@ -293001,8 +294133,19 @@ async function runCli(argv, println = console.log, progressln = console.error) {
293001
294133
  const name = flags.get("name");
293002
294134
  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
294135
  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`);
294136
+ const res = await svc.grantOwner(companyId, email2, name);
294137
+ if (!res.granted && res.existingOwner) {
294138
+ process.stderr.write(
294139
+ `\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
294140
+ \u6362\u4EBA\u8BF7\u8BA9\u73B0\u4EFB\u6301\u6709\u4EBA\u8C03 POST /api/companies/${companyId}/owner\uFF08\u8F6C\u4EA4\uFF09\u3002
294141
+ `
294142
+ );
294143
+ process.exitCode = 1;
294144
+ return;
294145
+ }
294146
+ println(
294147
+ 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`
294148
+ );
293006
294149
  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
294150
  return;
293008
294151
  }
@@ -294390,7 +295533,13 @@ ${res.warning}`);
294390
295533
  if (live.length > 0) {
294391
295534
  for (const r of live) println(`[design] \u5F15\u64CE\u5728\u8DD1\uFF1Arun ${r.runId.slice(0, 8)} \u72B6\u6001=${r.status}\u2014\u2014\u522B\u518D\u8D77\u65B0\u8F6E\uFF1B\u8FC7\u51E0\u5206\u949F\u518D status\uFF0C\u6216\u672C\u8F6E\u6536\u5DE5\u7B49\u91CD\u6D3E\u3002`);
294392
295535
  } else if (st.runs.length > 0) {
294393
- for (const r of st.runs.slice(0, 3)) println(`[design] \u8FD1\u671F run\uFF1A${r.runId.slice(0, 8)} \u7EC8\u6001=${r.status}`);
295536
+ for (const r of st.runs.slice(0, 3)) {
295537
+ println(`[design] \u8FD1\u671F run\uFF1A${r.runId.slice(0, 8)} \u7EC8\u6001=${r.status}${r.agentId ? ` \u5F15\u64CE=${r.agentId}` : ""}`);
295538
+ if (r.status === "failed" && r.error) {
295539
+ const reason = r.error.length > 300 ? `${r.error.slice(0, 300)}\u2026` : r.error;
295540
+ println(`[design] \u5931\u8D25\u539F\u56E0\uFF08od \u539F\u6587\uFF09\uFF1A${reason}`);
295541
+ }
295542
+ }
294394
295543
  } else {
294395
295544
  println(`[design] \u65E0\u5728\u4E16 run \u8BB0\u5F55\uFF08od \u91CD\u542F\u540E\u4E3A\u7A7A\uFF0C\u5C5E\u6B63\u5E38\uFF09\u2014\u2014\u770B\u4E0B\u9762\u677F\u4E0A\u4EA7\u7269\u5373\u6301\u4E45\u771F\u76F8\u3002`);
294396
295545
  }
@@ -294402,7 +295551,7 @@ ${res.warning}`);
294402
295551
  for (const a of st.artifacts) println(` - ${a.path}\uFF08${a.kind ?? "?"}\uFF09status=${a.status ?? "?"}${a.mtime ? ` \xB7 ${new Date(a.mtime).toISOString()}` : ""}`);
294403
295552
  if (flags.get("pull") === "true") {
294404
295553
  const odPublic = (flags.get("od-public-url") ?? process.env["OASIS_OPEN_DESIGN_PUBLIC_URL"] ?? "https://design.open-friday.com").replace(/\/$/, "");
294405
- const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, st.runs[0]?.runId ?? "status-pull");
295554
+ const { dir: dir2, entry } = await harvestBoard(odBase, odFetch, odPublic, os13.tmpdir(), st.boardKey, latestSucceededRun(st.runs)?.runId ?? "status-pull");
294406
295555
  println(`[design] \u5DF2\u62C9\u5230\u672C\u5730\uFF1A${dir2}\uFF08\u5165\u53E3 ${entry}\uFF09`);
294407
295556
  println(`[design] \u8BFB\u5B8C\u6EE1\u610F\u5373\u4EA4\u7A3F\uFF1Aoasis propose ${artifactId} --from-dir ${dir2} --reason "<\u8FD9\u7248\u505A\u4E86\u4EC0\u4E48>"`);
294408
295557
  } else {
@@ -296503,7 +297652,7 @@ function shimScript() {
296503
297652
  }
296504
297653
 
296505
297654
  // src/index.ts
296506
- var PKG_VERSION = true ? "2.2.80" : "dev";
297655
+ var PKG_VERSION = true ? "2.2.82" : "dev";
296507
297656
  var LOCAL_BIN = localBin();
296508
297657
  var NPM_PREFIX = npmPrefix();
296509
297658
  var INSTANCE = DEFAULT_INSTANCE;