@wairon/cli 5.1.1-dev.92 → 5.1.1-dev.93
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.
- package/dist/cli/index.js +552 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +214 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -65,7 +65,7 @@ var init_defaults = __esm({
|
|
|
65
65
|
copilot: ".github/prompts",
|
|
66
66
|
codex: ".codex/agents"
|
|
67
67
|
};
|
|
68
|
-
WAIRON_VERSION = "5.1.1-dev.
|
|
68
|
+
WAIRON_VERSION = "5.1.1-dev.93";
|
|
69
69
|
GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
70
70
|
SUPPORTED_ALIASES = ["wai"];
|
|
71
71
|
SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -212,6 +212,7 @@ __export(fs_exports, {
|
|
|
212
212
|
getProjectRootOverride: () => getProjectRootOverride,
|
|
213
213
|
getRequestParentReach: () => getRequestParentReach,
|
|
214
214
|
getRequestProjectRoot: () => getRequestProjectRoot,
|
|
215
|
+
getWriteReach: () => getWriteReach,
|
|
215
216
|
isOutsideRoot: () => isOutsideRoot,
|
|
216
217
|
listFiles: () => listFiles,
|
|
217
218
|
listFilesRecursive: () => listFilesRecursive,
|
|
@@ -222,7 +223,9 @@ __export(fs_exports, {
|
|
|
222
223
|
runWithProjectRoot: () => runWithProjectRoot,
|
|
223
224
|
setProjectRoot: () => setProjectRoot,
|
|
224
225
|
writeFile: () => writeFile,
|
|
225
|
-
writeFileIfChanged: () => writeFileIfChanged
|
|
226
|
+
writeFileIfChanged: () => writeFileIfChanged,
|
|
227
|
+
writeReachDenials: () => writeReachDenials,
|
|
228
|
+
writeReachPermits: () => writeReachPermits
|
|
226
229
|
});
|
|
227
230
|
function ensureDir(dirPath) {
|
|
228
231
|
fs3.mkdirSync(dirPath, { recursive: true });
|
|
@@ -277,6 +280,17 @@ function listFilesRecursive(dirPath, ext) {
|
|
|
277
280
|
}
|
|
278
281
|
return files;
|
|
279
282
|
}
|
|
283
|
+
function writeReachPermits(reach2, root, subsystemId) {
|
|
284
|
+
const own = subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === subsystemId && sameRoot(s.root, root));
|
|
285
|
+
if (own) return own.writable;
|
|
286
|
+
return !reach2.unwritableRoots.some((r) => sameRoot(r, root));
|
|
287
|
+
}
|
|
288
|
+
function writeReachDenials(reach2, root) {
|
|
289
|
+
return reach2.subsystems.filter((s) => !s.writable && sameRoot(s.root, root));
|
|
290
|
+
}
|
|
291
|
+
function getWriteReach() {
|
|
292
|
+
return requestRootStore.getStore()?.writeReach ?? null;
|
|
293
|
+
}
|
|
280
294
|
function currentRootBinding() {
|
|
281
295
|
return requestRootStore.getStore();
|
|
282
296
|
}
|
|
@@ -291,7 +305,8 @@ function runWithProjectBinding(dir, reach2, fn) {
|
|
|
291
305
|
parentReach: reach2.parentReach,
|
|
292
306
|
...reach2.narrowed ? { narrowed: true } : {},
|
|
293
307
|
...reach2.unwritableRoots ? { unwritableRoots: reach2.unwritableRoots.map((r) => path4.resolve(r)) } : {},
|
|
294
|
-
...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {}
|
|
308
|
+
...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {},
|
|
309
|
+
...reach2.writeReach ? { writeReach: reach2.writeReach } : {}
|
|
295
310
|
},
|
|
296
311
|
fn
|
|
297
312
|
);
|
|
@@ -369,7 +384,7 @@ function aiDirAt(root, ...segments) {
|
|
|
369
384
|
const base = !fs3.existsSync(waiPath) && fs3.existsSync(waironPath) ? waironPath : waiPath;
|
|
370
385
|
return path4.join(base, ...segments);
|
|
371
386
|
}
|
|
372
|
-
var fs3, path4, import_node_async_hooks, projectRootOverride, requestRootStore;
|
|
387
|
+
var fs3, path4, import_node_async_hooks, projectRootOverride, sameRoot, requestRootStore;
|
|
373
388
|
var init_fs = __esm({
|
|
374
389
|
"src/utils/fs.ts"() {
|
|
375
390
|
"use strict";
|
|
@@ -377,6 +392,7 @@ var init_fs = __esm({
|
|
|
377
392
|
path4 = __toESM(require("path"));
|
|
378
393
|
import_node_async_hooks = require("async_hooks");
|
|
379
394
|
projectRootOverride = null;
|
|
395
|
+
sameRoot = (a, b) => path4.resolve(a) === path4.resolve(b);
|
|
380
396
|
requestRootStore = new import_node_async_hooks.AsyncLocalStorage();
|
|
381
397
|
}
|
|
382
398
|
});
|
|
@@ -24970,6 +24986,28 @@ var init_exports2 = __esm({
|
|
|
24970
24986
|
});
|
|
24971
24987
|
|
|
24972
24988
|
// src/core/specs.ts
|
|
24989
|
+
function deniedOwners(reach2, owners) {
|
|
24990
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24991
|
+
const denied = [];
|
|
24992
|
+
for (const o of owners) {
|
|
24993
|
+
const key = `${path23.resolve(o.root)}|${o.subsystemId ?? ""}`;
|
|
24994
|
+
if (seen.has(key)) continue;
|
|
24995
|
+
seen.add(key);
|
|
24996
|
+
if (writeReachPermits(reach2, o.root, o.subsystemId)) continue;
|
|
24997
|
+
const rule = o.subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === o.subsystemId && path23.resolve(s.root) === path23.resolve(o.root));
|
|
24998
|
+
denied.push(o.subsystemId === null ? `project "${o.project}" itself (a spec of no subsystem, decided at the project rung)` : `subsystem "${o.subsystemId}" of project "${o.project}" (decided by ${rule?.decidedBy ?? "the project rung"})`);
|
|
24999
|
+
}
|
|
25000
|
+
return denied;
|
|
25001
|
+
}
|
|
25002
|
+
function assertSpecWritesPermitted(owners, what) {
|
|
25003
|
+
const reach2 = getWriteReach();
|
|
25004
|
+
if (!reach2) return;
|
|
25005
|
+
const denied = deniedOwners(reach2, owners);
|
|
25006
|
+
if (denied.length === 0) return;
|
|
25007
|
+
throw new SubsystemWriteDenied(
|
|
25008
|
+
`SubsystemWriteDenied: ${what} would change ${denied.length === 1 ? "a spec" : "specs"} this request may not change \u2014 ${denied.join("; ")}. Nothing was written.`
|
|
25009
|
+
);
|
|
25010
|
+
}
|
|
24973
25011
|
function unknownKeysIn(raw, parsed, at, out) {
|
|
24974
25012
|
if (Array.isArray(raw)) {
|
|
24975
25013
|
if (!Array.isArray(parsed) || parsed.length !== raw.length) return;
|
|
@@ -26686,10 +26724,18 @@ function normalizeReferences(kind, id) {
|
|
|
26686
26724
|
function rewriteReferences(kind, id, edits) {
|
|
26687
26725
|
return current().respellReferences(kind, id, edits);
|
|
26688
26726
|
}
|
|
26727
|
+
function assertSpecsInReach(targets, what, projectRung = false) {
|
|
26728
|
+
if (!getWriteReach()) return;
|
|
26729
|
+
const ws = current();
|
|
26730
|
+
const writable = ["system", "subsystem", "component", "interface", "implementation", "type"];
|
|
26731
|
+
const owners = targets.map((t) => ws.specOwner(t.kind, t.kind === "system" ? null : (writable.includes(t.kind) ? ws.load(t.kind, t.id) : null) ?? { id: t.id }));
|
|
26732
|
+
if (projectRung) owners.push(ws.specOwner("system", null));
|
|
26733
|
+
assertSpecWritesPermitted(owners, what);
|
|
26734
|
+
}
|
|
26689
26735
|
function moveMethods(from, to, methods, hooks, dryRun) {
|
|
26690
26736
|
return current().moveMethods(from, to, methods, hooks, dryRun);
|
|
26691
26737
|
}
|
|
26692
|
-
var fs16, path23, crypto5, PartReadOnly, PART_FORBIDDEN_FIELDS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
|
|
26738
|
+
var fs16, path23, crypto5, PartReadOnly, SubsystemWriteDenied, PART_FORBIDDEN_FIELDS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
|
|
26693
26739
|
var init_specs2 = __esm({
|
|
26694
26740
|
"src/core/specs.ts"() {
|
|
26695
26741
|
"use strict";
|
|
@@ -26722,6 +26768,12 @@ var init_specs2 = __esm({
|
|
|
26722
26768
|
this.name = "PartReadOnly";
|
|
26723
26769
|
}
|
|
26724
26770
|
};
|
|
26771
|
+
SubsystemWriteDenied = class extends WaironError {
|
|
26772
|
+
constructor(message) {
|
|
26773
|
+
super(message);
|
|
26774
|
+
this.name = "SubsystemWriteDenied";
|
|
26775
|
+
}
|
|
26776
|
+
};
|
|
26725
26777
|
PART_FORBIDDEN_FIELDS = ["members", "externals", "rules", "extensions", "composition", "targets"];
|
|
26726
26778
|
IMPORTABLE_POSITIONS = /* @__PURE__ */ new Set(["dependsOn", "dispatch", "mounts", "narrative"]);
|
|
26727
26779
|
SIGNATURE_TTL_MS = 2e3;
|
|
@@ -27397,6 +27449,92 @@ var init_specs2 = __esm({
|
|
|
27397
27449
|
`The spec file ${file} belongs to the part "${held.part.alias}", fetched from git (${held.source?.git ?? "its repository"} at ${held.part.commit ?? "its pinned commit"}): a git part is read-only here. Edit it in its own repository, commit it there, and take it in with \`wairon member update ${held.part.alias}\`. Nothing was written.`
|
|
27398
27450
|
);
|
|
27399
27451
|
}
|
|
27452
|
+
/**
|
|
27453
|
+
* spec_registry.save step 5 / delete step 2: inside a hosted request, may the
|
|
27454
|
+
* request change this spec? Judged against the request's write reach with the
|
|
27455
|
+
* home's project root and the spec's owning subsystem — the document written
|
|
27456
|
+
* and, when a stored document is replaced or removed, the one stored (a
|
|
27457
|
+
* component moved out of a subsystem changes that subsystem too). Outside a
|
|
27458
|
+
* hosted request nothing is judged.
|
|
27459
|
+
*/
|
|
27460
|
+
assertHomeInReach(file, kind, document) {
|
|
27461
|
+
if (!getWriteReach()) return;
|
|
27462
|
+
const raw = this.homeRootOf(file);
|
|
27463
|
+
const owners = [];
|
|
27464
|
+
if (document !== null) owners.push(this.documentOwner(raw, kind, document));
|
|
27465
|
+
if (pathExists(file)) {
|
|
27466
|
+
try {
|
|
27467
|
+
owners.push(this.documentOwner(raw, kind, readSpecFile(file)));
|
|
27468
|
+
} catch {
|
|
27469
|
+
}
|
|
27470
|
+
}
|
|
27471
|
+
assertSpecWritesPermitted(owners, `writing ${kind} spec ${path23.basename(path23.dirname(file))}/${path23.basename(file)}`);
|
|
27472
|
+
}
|
|
27473
|
+
/** The project root a spec file is homed under: the root whose specs folder, or one of whose parts' folders, holds it (deepest wins). */
|
|
27474
|
+
homeRootOf(file) {
|
|
27475
|
+
this.scanAll();
|
|
27476
|
+
let best;
|
|
27477
|
+
let depth = -1;
|
|
27478
|
+
for (const raw of this.cachedRawRoots) {
|
|
27479
|
+
const homes = [aiPathsAt(raw.dir).specsDir(), ...raw.record.parts.map((p) => p.directory).filter((d) => d !== void 0)];
|
|
27480
|
+
for (const home of homes) {
|
|
27481
|
+
if (isWithin(home, file) && path23.resolve(home).length > depth) {
|
|
27482
|
+
best = raw;
|
|
27483
|
+
depth = path23.resolve(home).length;
|
|
27484
|
+
}
|
|
27485
|
+
}
|
|
27486
|
+
}
|
|
27487
|
+
if (!best) return { dir: this.rootDir, namespace: "", project: path23.basename(this.rootDir) };
|
|
27488
|
+
return { dir: best.dir, namespace: best.record.namespace, project: idOf(best) ?? (best.record.namespace || path23.basename(best.dir)) };
|
|
27489
|
+
}
|
|
27490
|
+
/**
|
|
27491
|
+
* The owner of one spec DOCUMENT as stored in its root (its references local
|
|
27492
|
+
* to that root): a subsystem is its own, a component or type names its
|
|
27493
|
+
* subsystem, an interface or implementation follows its component; the L0, a
|
|
27494
|
+
* system-level type and a type group belong to no subsystem.
|
|
27495
|
+
*/
|
|
27496
|
+
documentOwner(root, kind, document) {
|
|
27497
|
+
const doc = document ?? {};
|
|
27498
|
+
const local = (v) => typeof v === "string" && v ? splitNamespace(v).localId : null;
|
|
27499
|
+
const key = (v) => typeof v === "string" && v ? keyIn(root.namespace, splitNamespace(v).localId) : null;
|
|
27500
|
+
let subsystemId = null;
|
|
27501
|
+
if (kind === "subsystem") subsystemId = local(doc.id);
|
|
27502
|
+
else if (kind === "component" || kind === "type") subsystemId = local(doc.subsystem);
|
|
27503
|
+
else if (kind === "interface") subsystemId = this.componentSubsystem(key(doc.component));
|
|
27504
|
+
else if (kind === "implementation") {
|
|
27505
|
+
const contract = key(doc.contract);
|
|
27506
|
+
subsystemId = this.componentSubsystem(contract ? this.loadInterfaceSpec(contract)?.component ?? null : null);
|
|
27507
|
+
}
|
|
27508
|
+
return { root: root.dir, project: root.project, subsystemId };
|
|
27509
|
+
}
|
|
27510
|
+
/** The local id of a component's subsystem, by the component's key; null when no such component is stored. */
|
|
27511
|
+
componentSubsystem(componentKey) {
|
|
27512
|
+
if (!componentKey) return null;
|
|
27513
|
+
const subsystem = this.loadComponentSpec(componentKey)?.subsystem;
|
|
27514
|
+
return subsystem ? splitNamespace(subsystem).localId : null;
|
|
27515
|
+
}
|
|
27516
|
+
/**
|
|
27517
|
+
* The owner of one IN-MEMORY spec (its references keys, as the loader binds
|
|
27518
|
+
* them) — what a multi-spec write (a rename, a method move) judges before its
|
|
27519
|
+
* first write. `staged` answers a spec the write itself creates.
|
|
27520
|
+
*/
|
|
27521
|
+
specOwner(kind, spec, staged) {
|
|
27522
|
+
this.scanAll();
|
|
27523
|
+
const id = kind === "system" ? "" : String(spec?.id ?? "");
|
|
27524
|
+
const namespace = (kind === "system" ? "" : this.ownerKeyOf(id)) ?? "";
|
|
27525
|
+
const raw = this.cachedRawRoots.find((r) => r.record.namespace === namespace);
|
|
27526
|
+
const root = raw ? { dir: raw.dir, project: idOf(raw) ?? (namespace || path23.basename(raw.dir)) } : { dir: this.rootDir, project: path23.basename(this.rootDir) };
|
|
27527
|
+
const localOfKey = (v) => v ? splitNamespace(v).localId : null;
|
|
27528
|
+
let subsystemId = null;
|
|
27529
|
+
if (kind === "subsystem") subsystemId = localOfKey(id);
|
|
27530
|
+
else if (kind === "component" || kind === "type") subsystemId = localOfKey(spec?.subsystem);
|
|
27531
|
+
else if (kind === "interface") subsystemId = this.componentSubsystem(spec?.component ?? null);
|
|
27532
|
+
else if (kind === "implementation" && spec?.contract) {
|
|
27533
|
+
const component = this.loadInterfaceSpec(spec.contract)?.component ?? staged?.("interface", spec.contract)?.component ?? null;
|
|
27534
|
+
subsystemId = this.componentSubsystem(component);
|
|
27535
|
+
}
|
|
27536
|
+
return { root: root.dir, project: root.project, subsystemId };
|
|
27537
|
+
}
|
|
27400
27538
|
/**
|
|
27401
27539
|
* The key one spec file is approved under (stage 8): its path from the bound
|
|
27402
27540
|
* root, POSIX — or, for a part's file, `members/<alias>/<path inside the
|
|
@@ -27407,13 +27545,15 @@ var init_specs2 = __esm({
|
|
|
27407
27545
|
return approvalKeyIn(file, this.rootDir, this.cachedRoots[0]?.parts ?? []);
|
|
27408
27546
|
}
|
|
27409
27547
|
/** The file store's write, at a home the writer resolved — a part's folder included, a git part's refused. */
|
|
27410
|
-
writeHome(file, document) {
|
|
27548
|
+
writeHome(file, document, kind) {
|
|
27411
27549
|
this.assertHomeWritable(file);
|
|
27550
|
+
this.assertHomeInReach(file, kind, document);
|
|
27412
27551
|
writeSpecFile(file, document);
|
|
27413
27552
|
}
|
|
27414
27553
|
/** The file store's delete, pruning up to the specs folder the file lives in (its part's, for a part's file). */
|
|
27415
|
-
removeHome(file) {
|
|
27554
|
+
removeHome(file, kind) {
|
|
27416
27555
|
this.assertHomeWritable(file);
|
|
27556
|
+
this.assertHomeInReach(file, kind, null);
|
|
27417
27557
|
const part = this.partHolding(file)?.part;
|
|
27418
27558
|
return removeSpecFile(file, part?.directory ? aiPathsAt(part.directory).specsDir() : this.paths.specsDir());
|
|
27419
27559
|
}
|
|
@@ -27764,6 +27904,7 @@ var init_specs2 = __esm({
|
|
|
27764
27904
|
}
|
|
27765
27905
|
saveSystemSpec(spec) {
|
|
27766
27906
|
const p = this.paths.specsSystem();
|
|
27907
|
+
this.assertHomeInReach(p, "system", spec);
|
|
27767
27908
|
ensureDir(path23.dirname(p));
|
|
27768
27909
|
writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
|
|
27769
27910
|
invalidateSpecCache();
|
|
@@ -27914,12 +28055,12 @@ var init_specs2 = __esm({
|
|
|
27914
28055
|
}
|
|
27915
28056
|
}
|
|
27916
28057
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
27917
|
-
this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
|
|
28058
|
+
this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id), "subsystem");
|
|
27918
28059
|
invalidateSpecCache();
|
|
27919
28060
|
}
|
|
27920
28061
|
deleteSubsystemSpec(id) {
|
|
27921
28062
|
const p = this.getSubsystemPath(id);
|
|
27922
|
-
if (!this.removeHome(p)) return false;
|
|
28063
|
+
if (!this.removeHome(p, "subsystem")) return false;
|
|
27923
28064
|
invalidateSpecCache();
|
|
27924
28065
|
return true;
|
|
27925
28066
|
}
|
|
@@ -27974,14 +28115,14 @@ var init_specs2 = __esm({
|
|
|
27974
28115
|
);
|
|
27975
28116
|
}
|
|
27976
28117
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
27977
|
-
this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
|
|
28118
|
+
this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id), "component");
|
|
27978
28119
|
invalidateSpecCache();
|
|
27979
28120
|
this.normalizeComponentLayout();
|
|
27980
28121
|
return notices;
|
|
27981
28122
|
}
|
|
27982
28123
|
deleteComponentSpec(id) {
|
|
27983
28124
|
const p = this.getComponentPath(id);
|
|
27984
|
-
if (!this.removeHome(p)) return false;
|
|
28125
|
+
if (!this.removeHome(p, "component")) return false;
|
|
27985
28126
|
invalidateSpecCache();
|
|
27986
28127
|
return true;
|
|
27987
28128
|
}
|
|
@@ -28103,13 +28244,13 @@ var init_specs2 = __esm({
|
|
|
28103
28244
|
}
|
|
28104
28245
|
}
|
|
28105
28246
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
28106
|
-
this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
|
|
28247
|
+
this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id), "interface");
|
|
28107
28248
|
invalidateSpecCache();
|
|
28108
28249
|
return notices;
|
|
28109
28250
|
}
|
|
28110
28251
|
deleteInterfaceSpec(id) {
|
|
28111
28252
|
const p = this.getInterfacePath(id);
|
|
28112
|
-
if (!this.removeHome(p)) return false;
|
|
28253
|
+
if (!this.removeHome(p, "interface")) return false;
|
|
28113
28254
|
invalidateSpecCache();
|
|
28114
28255
|
return true;
|
|
28115
28256
|
}
|
|
@@ -28159,13 +28300,13 @@ var init_specs2 = __esm({
|
|
|
28159
28300
|
}
|
|
28160
28301
|
}
|
|
28161
28302
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
28162
|
-
this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
|
|
28303
|
+
this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id), "implementation");
|
|
28163
28304
|
invalidateSpecCache();
|
|
28164
28305
|
return notices;
|
|
28165
28306
|
}
|
|
28166
28307
|
deleteImplementationSpec(id) {
|
|
28167
28308
|
const p = this.getImplementationPath(id);
|
|
28168
|
-
if (!this.removeHome(p)) return false;
|
|
28309
|
+
if (!this.removeHome(p, "implementation")) return false;
|
|
28169
28310
|
invalidateSpecCache();
|
|
28170
28311
|
return true;
|
|
28171
28312
|
}
|
|
@@ -28211,14 +28352,14 @@ var init_specs2 = __esm({
|
|
|
28211
28352
|
}
|
|
28212
28353
|
}
|
|
28213
28354
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
28214
|
-
this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
|
|
28355
|
+
this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id), "type");
|
|
28215
28356
|
invalidateSpecCache();
|
|
28216
28357
|
return notices;
|
|
28217
28358
|
}
|
|
28218
28359
|
deleteTypeSpec(id) {
|
|
28219
28360
|
const spec = this.loadTypeSpec(id);
|
|
28220
28361
|
const p = this.getTypePath(id, spec?.subsystem, spec?.group);
|
|
28221
|
-
if (!this.removeHome(p)) return false;
|
|
28362
|
+
if (!this.removeHome(p, "type")) return false;
|
|
28222
28363
|
invalidateSpecCache();
|
|
28223
28364
|
return true;
|
|
28224
28365
|
}
|
|
@@ -28241,12 +28382,12 @@ var init_specs2 = __esm({
|
|
|
28241
28382
|
specToWrite.createdAt = existing.createdAt;
|
|
28242
28383
|
}
|
|
28243
28384
|
specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
28244
|
-
this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
|
|
28385
|
+
this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id), "group");
|
|
28245
28386
|
invalidateSpecCache();
|
|
28246
28387
|
}
|
|
28247
28388
|
deleteGroupSpec(id) {
|
|
28248
28389
|
const p = this.getGroupPath(id);
|
|
28249
|
-
if (!this.removeHome(p)) return false;
|
|
28390
|
+
if (!this.removeHome(p, "group")) return false;
|
|
28250
28391
|
invalidateSpecCache();
|
|
28251
28392
|
return true;
|
|
28252
28393
|
}
|
|
@@ -28444,7 +28585,7 @@ var init_specs2 = __esm({
|
|
|
28444
28585
|
);
|
|
28445
28586
|
}
|
|
28446
28587
|
if (JSON.stringify(document) === JSON.stringify(stored)) return false;
|
|
28447
|
-
this.writeHome(file, document);
|
|
28588
|
+
this.writeHome(file, document, kind);
|
|
28448
28589
|
invalidateSpecCache();
|
|
28449
28590
|
return true;
|
|
28450
28591
|
}
|
|
@@ -29358,6 +29499,7 @@ var init_specs2 = __esm({
|
|
|
29358
29499
|
}
|
|
29359
29500
|
}
|
|
29360
29501
|
const carried = projectConfigRepository.rekeyCarried(rename, true);
|
|
29502
|
+
this.assertMoveInReach(from, to, plan7, carried.length > 0);
|
|
29361
29503
|
if (dryRun) {
|
|
29362
29504
|
const edits2 = moveChangeReports([...plan7.edits.values()], true);
|
|
29363
29505
|
return {
|
|
@@ -29397,6 +29539,14 @@ var init_specs2 = __esm({
|
|
|
29397
29539
|
carried
|
|
29398
29540
|
};
|
|
29399
29541
|
}
|
|
29542
|
+
/** Refuse a method move, naming every subsystem it may not change, before anything is written. */
|
|
29543
|
+
assertMoveInReach(from, to, plan7, rekeysRegister) {
|
|
29544
|
+
if (!getWriteReach()) return;
|
|
29545
|
+
const staged = (kind, id) => plan7.edits.get(`${kind}:${id}`)?.after;
|
|
29546
|
+
const owners = [...plan7.edits.values()].flatMap((e) => [e.before, e.after].filter((s) => s !== null && s !== void 0).map((s) => this.specOwner(e.kind, s, staged)));
|
|
29547
|
+
if (rekeysRegister) owners.push(this.specOwner("system", null));
|
|
29548
|
+
assertSpecWritesPermitted(owners, `moving methods from "${from}" to "${to}"`);
|
|
29549
|
+
}
|
|
29400
29550
|
/**
|
|
29401
29551
|
* Rekey every lint allow keyed on a moved method, staging each spec it
|
|
29402
29552
|
* changes like any other reference the move follows. An allow anchored at a
|
|
@@ -31346,7 +31496,7 @@ function restateReExports(from, to) {
|
|
|
31346
31496
|
publicInterfaces: entries.map((e) => e.from === from ? { ...e, from: to } : e)
|
|
31347
31497
|
});
|
|
31348
31498
|
}
|
|
31349
|
-
function rewriteRefFields(specsDir, remap, excludeDir) {
|
|
31499
|
+
function rewriteRefFields(specsDir, remap, excludeDir, dryRun = false) {
|
|
31350
31500
|
const rewritten = [];
|
|
31351
31501
|
for (const file of listFilesRecursive(specsDir, ".yaml")) {
|
|
31352
31502
|
if (excludeDir && isWithinDir2(excludeDir, file)) continue;
|
|
@@ -31359,12 +31509,12 @@ function rewriteRefFields(specsDir, remap, excludeDir) {
|
|
|
31359
31509
|
if (!raw || typeof raw !== "object") continue;
|
|
31360
31510
|
const kind = specKind(raw);
|
|
31361
31511
|
if (!rewriteSpecRefs(raw, remap) || !kind) continue;
|
|
31362
|
-
writeYamlFile(file, raw);
|
|
31512
|
+
if (!dryRun) writeYamlFile(file, raw);
|
|
31363
31513
|
rewritten.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
|
|
31364
31514
|
}
|
|
31365
31515
|
return rewritten;
|
|
31366
31516
|
}
|
|
31367
|
-
function rekeyLintAllows(specsDir, rename) {
|
|
31517
|
+
function rekeyLintAllows(specsDir, rename, dryRun = false) {
|
|
31368
31518
|
const rewritten = [];
|
|
31369
31519
|
for (const file of listFilesRecursive(specsDir, ".yaml")) {
|
|
31370
31520
|
let raw;
|
|
@@ -31395,7 +31545,7 @@ function rekeyLintAllows(specsDir, rename) {
|
|
|
31395
31545
|
}
|
|
31396
31546
|
}
|
|
31397
31547
|
if (!changed) continue;
|
|
31398
|
-
writeYamlFile(file, raw);
|
|
31548
|
+
if (!dryRun) writeYamlFile(file, raw);
|
|
31399
31549
|
rewritten.push({ kind, id: specId });
|
|
31400
31550
|
}
|
|
31401
31551
|
return rewritten;
|
|
@@ -31473,15 +31623,22 @@ function renameComponent(componentId, newId) {
|
|
|
31473
31623
|
specs: renamed.map(({ from, to }) => ({ from, to })),
|
|
31474
31624
|
components: [{ from: componentId, to: newId }]
|
|
31475
31625
|
};
|
|
31476
|
-
projectConfigRepository.rekeyCarried(rename, true);
|
|
31477
|
-
const
|
|
31626
|
+
const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
|
|
31627
|
+
const remap = (ref, position) => {
|
|
31478
31628
|
if (position === "component" || position === "entity-class" || position === "auth-source") {
|
|
31479
31629
|
return ref === componentId ? newId : ref;
|
|
31480
31630
|
}
|
|
31481
31631
|
if (position === "interface" && ref === movingInterface?.id) return interfaceId;
|
|
31482
31632
|
return ref;
|
|
31483
|
-
}
|
|
31484
|
-
const
|
|
31633
|
+
};
|
|
31634
|
+
const specsDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
31635
|
+
assertSpecsInReach(
|
|
31636
|
+
[...renamed.map(({ kind, from }) => ({ kind, id: from })), ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)],
|
|
31637
|
+
`renaming component "${componentId}"`,
|
|
31638
|
+
Array.isArray(registerEdits) && registerEdits.length > 0
|
|
31639
|
+
);
|
|
31640
|
+
const rewrittenRefs = rewriteRefFields(specsDir, remap);
|
|
31641
|
+
const allowsRekeyed = rekeyLintAllows(specsDir, rename);
|
|
31485
31642
|
const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
|
|
31486
31643
|
invalidateSpecCache();
|
|
31487
31644
|
moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation);
|
|
@@ -31559,7 +31716,18 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
|
|
|
31559
31716
|
]
|
|
31560
31717
|
}]
|
|
31561
31718
|
};
|
|
31562
|
-
projectConfigRepository.rekeyCarried(rename, true);
|
|
31719
|
+
const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
|
|
31720
|
+
const methodRemap = (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref;
|
|
31721
|
+
const reachDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
31722
|
+
assertSpecsInReach(
|
|
31723
|
+
[
|
|
31724
|
+
...(rename.methods?.[0]?.specs ?? []).map((id) => ({ kind: moving.some((i) => i.id === id) ? "interface" : "implementation", id })),
|
|
31725
|
+
...rewriteRefFields(reachDir, methodRemap, void 0, true),
|
|
31726
|
+
...rekeyLintAllows(reachDir, rename, true)
|
|
31727
|
+
],
|
|
31728
|
+
`renaming method "${componentId}.${methodName}"`,
|
|
31729
|
+
Array.isArray(registerEdits) && registerEdits.length > 0
|
|
31730
|
+
);
|
|
31563
31731
|
for (const contract of moving) {
|
|
31564
31732
|
saveInterfaceSpec({
|
|
31565
31733
|
...contract,
|
|
@@ -31582,7 +31750,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
|
|
|
31582
31750
|
}
|
|
31583
31751
|
invalidateSpecCache();
|
|
31584
31752
|
const specsDir = aiPathsAt(getProjectRoot()).specsDir();
|
|
31585
|
-
const retargeted = rewriteRefFields(specsDir,
|
|
31753
|
+
const retargeted = rewriteRefFields(specsDir, methodRemap);
|
|
31586
31754
|
const allowsRekeyed = rekeyLintAllows(specsDir, rename);
|
|
31587
31755
|
const rewritten = [...new Set([...retargeted, ...allowsRekeyed].map((spec) => spec.id))];
|
|
31588
31756
|
const mentions = collectMentions(specsDir, methodName, moving);
|
|
@@ -41386,10 +41554,13 @@ function rehearse2(plan7) {
|
|
|
41386
41554
|
return { ...plan7, refusals: [...plan7.refusals, { code: `${plan7.request.verb}-refused`, project: "", detail }] };
|
|
41387
41555
|
}
|
|
41388
41556
|
const changes = diff(rehearsal);
|
|
41389
|
-
const
|
|
41390
|
-
|
|
41557
|
+
const refused2 = [
|
|
41558
|
+
...unwritableOwners(changes).map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` })),
|
|
41559
|
+
...deniedSubsystems(changes)
|
|
41560
|
+
];
|
|
41561
|
+
if (refused2.length > 0) {
|
|
41391
41562
|
discard(rehearsal);
|
|
41392
|
-
return { ...plan7, refusals: [...plan7.refusals, ...
|
|
41563
|
+
return { ...plan7, refusals: [...plan7.refusals, ...refused2] };
|
|
41393
41564
|
}
|
|
41394
41565
|
return { ...plan7, rehearsal, changes, relock: relockOf(plan7, changes) };
|
|
41395
41566
|
}
|
|
@@ -41433,6 +41604,16 @@ function unwritableOwners(changes) {
|
|
|
41433
41604
|
const owners = [...new Set(changes.map((c) => path48.resolve(c.project)))];
|
|
41434
41605
|
return owners.filter((o) => denied.includes(o));
|
|
41435
41606
|
}
|
|
41607
|
+
function deniedSubsystems(changes) {
|
|
41608
|
+
const reach2 = getWriteReach();
|
|
41609
|
+
if (!reach2) return [];
|
|
41610
|
+
const owners = [...new Set(changes.map((c) => path48.resolve(c.project)))];
|
|
41611
|
+
return owners.flatMap((o) => writeReachDenials(reach2, o).map((s) => ({
|
|
41612
|
+
code: "subsystem-denied",
|
|
41613
|
+
project: o,
|
|
41614
|
+
detail: `the plan must change ${o}, which holds subsystem "${s.subsystemId}" this request may not change (decided by ${s.decidedBy}); a family migration reshapes its projects whole, so it needs write on every subsystem of every project it changes`
|
|
41615
|
+
})));
|
|
41616
|
+
}
|
|
41436
41617
|
function pathProducers(plan7, family) {
|
|
41437
41618
|
const inside = (dir) => {
|
|
41438
41619
|
const rel2 = path48.relative(plan7.familyRoot, dir);
|
|
@@ -48465,6 +48646,27 @@ function existingProjectRoot(dataDir, id) {
|
|
|
48465
48646
|
const rec = records.find((r) => r.id === id);
|
|
48466
48647
|
return rec ? derivedRoot(records, rec) : null;
|
|
48467
48648
|
}
|
|
48649
|
+
function declaredSubsystemIds(dataDir, id) {
|
|
48650
|
+
const root = rootOnDisk(dataDir, id);
|
|
48651
|
+
return root ? subsystemIdsAt(root) : null;
|
|
48652
|
+
}
|
|
48653
|
+
function rootOnDisk(dataDir, id) {
|
|
48654
|
+
if (!isValidProjectId(id)) return null;
|
|
48655
|
+
const records = load5(dataDir);
|
|
48656
|
+
const rec = records.find((r) => r.id === id);
|
|
48657
|
+
const root = rec ? derivedRoot(records, rec) : null;
|
|
48658
|
+
return root && fs39.existsSync(root) ? root : null;
|
|
48659
|
+
}
|
|
48660
|
+
function subsystemIdsAt(dir) {
|
|
48661
|
+
return runWithProjectRoot(dir, () => loadSubsystemSpecs()).map((s) => s.id).filter((key) => !key.includes("::"));
|
|
48662
|
+
}
|
|
48663
|
+
function partSubsystemIds(dataDir, id, alias) {
|
|
48664
|
+
const root = rootOnDisk(dataDir, id);
|
|
48665
|
+
const decl = root ? memberDeclarationsOn(root).find((d) => d.alias === alias && d.path && !d.hosted && !d.refused) : void 0;
|
|
48666
|
+
if (!root || !decl?.path) return null;
|
|
48667
|
+
const dir = assertContainedProjectPath(root, decl.path);
|
|
48668
|
+
return fs39.existsSync(dir) ? subsystemIdsAt(dir) : null;
|
|
48669
|
+
}
|
|
48468
48670
|
function createProjectRecord(dataDir, id) {
|
|
48469
48671
|
if (!isValidProjectId(id)) {
|
|
48470
48672
|
throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
|
|
@@ -49382,6 +49584,23 @@ var fs42 = __toESM(require("fs"));
|
|
|
49382
49584
|
var path64 = __toESM(require("path"));
|
|
49383
49585
|
|
|
49384
49586
|
// src/server/types.ts
|
|
49587
|
+
var SUBSYSTEM_SCOPE_SEPARATOR = "/";
|
|
49588
|
+
var SUBSYSTEM_NOT_FOUND = "subsystem not found";
|
|
49589
|
+
function parseSubsystemScope(scopeId) {
|
|
49590
|
+
const at = scopeId.indexOf(SUBSYSTEM_SCOPE_SEPARATOR);
|
|
49591
|
+
if (at <= 0 || at === scopeId.length - 1) return null;
|
|
49592
|
+
return { projectId: scopeId.slice(0, at), subsystemId: scopeId.slice(at + 1) };
|
|
49593
|
+
}
|
|
49594
|
+
function describeDecision(p) {
|
|
49595
|
+
const setting = { "instance-admin": "the instance-admin bypass", user: "a user setting", role: "a role", "everyone-default": "the everyone-default", "instance-default": "the instance default" }[p.source];
|
|
49596
|
+
if (p.source === "instance-admin" || p.source === "instance-default") return setting;
|
|
49597
|
+
if (p.decidedScopeKind === "subsystem" && p.decidedScopeId) {
|
|
49598
|
+
const parsed = parseSubsystemScope(p.decidedScopeId);
|
|
49599
|
+
return `${setting} at subsystem ${parsed?.subsystemId ?? p.decidedScopeId} of project ${parsed?.projectId ?? ""}`.trim();
|
|
49600
|
+
}
|
|
49601
|
+
if (p.decidedScopeKind && p.decidedScopeKind !== "instance") return `${setting} at ${p.decidedScopeKind} ${p.decidedScopeId ?? ""}`.trim();
|
|
49602
|
+
return `${setting} at the instance`;
|
|
49603
|
+
}
|
|
49385
49604
|
var SSO_ADMIN_ROLE_ID = "sso-admin";
|
|
49386
49605
|
var UNAUTHENTICATED = {
|
|
49387
49606
|
tokenId: "",
|
|
@@ -49491,6 +49710,7 @@ function listRoles(dataDir) {
|
|
|
49491
49710
|
}
|
|
49492
49711
|
|
|
49493
49712
|
// src/server/permission-rules.ts
|
|
49713
|
+
var SUBSYSTEM_CAPABILITY = "project:write";
|
|
49494
49714
|
var INSTANCE_ROOT = { kind: "instance" };
|
|
49495
49715
|
var scopeKey = (kind, id) => `${kind}:${id ?? ""}`;
|
|
49496
49716
|
function isDefinitive(value) {
|
|
@@ -49528,7 +49748,13 @@ function familyRootUnits(projectId, world) {
|
|
|
49528
49748
|
const placement = world.placements.find((p) => p.projectId === familyRoot);
|
|
49529
49749
|
return placement ? unitChain(placement.unitId, world) : [];
|
|
49530
49750
|
}
|
|
49531
|
-
function ancestorChain(targetScopeKind, targetScopeId, world) {
|
|
49751
|
+
function ancestorChain(capability, targetScopeKind, targetScopeId, world) {
|
|
49752
|
+
if (targetScopeKind === "subsystem") {
|
|
49753
|
+
const parsed = parseSubsystemScope(targetScopeId);
|
|
49754
|
+
if (!parsed) return [INSTANCE_ROOT];
|
|
49755
|
+
const own = capability === SUBSYSTEM_CAPABILITY ? [{ kind: "subsystem", id: targetScopeId }] : [];
|
|
49756
|
+
return [...own, ...ancestorChain(capability, "project", parsed.projectId, world)];
|
|
49757
|
+
}
|
|
49532
49758
|
if (targetScopeKind === "project") {
|
|
49533
49759
|
const projects = projectChain(targetScopeId, world).map((id) => ({ kind: "project", id }));
|
|
49534
49760
|
return [...projects, ...familyRootUnits(targetScopeId, world), INSTANCE_ROOT];
|
|
@@ -49570,7 +49796,8 @@ function subjectIdMatches(subject, assignmentSubjectId) {
|
|
|
49570
49796
|
if (assignmentSubjectId === subject.subjectId) return true;
|
|
49571
49797
|
return (subject.aliasSubjectIds ?? []).includes(assignmentSubjectId);
|
|
49572
49798
|
}
|
|
49573
|
-
function decideScope(settings) {
|
|
49799
|
+
function decideScope(settings, kind) {
|
|
49800
|
+
if (kind === "subsystem") return decideSubsystemScope(settings);
|
|
49574
49801
|
if (isDefinitive(settings.user)) return { value: settings.user, source: "user" };
|
|
49575
49802
|
const best = settings.roleValues.reduce(
|
|
49576
49803
|
(acc, v) => acc === void 0 || grantRank(v) > grantRank(acc) ? v : acc,
|
|
@@ -49580,14 +49807,21 @@ function decideScope(settings) {
|
|
|
49580
49807
|
if (isDefinitive(settings.everyone)) return { value: settings.everyone, source: "everyone-default" };
|
|
49581
49808
|
return null;
|
|
49582
49809
|
}
|
|
49810
|
+
function decideSubsystemScope(settings) {
|
|
49811
|
+
const yesOrNo = (v) => v === "yes" || v === "no";
|
|
49812
|
+
if (yesOrNo(settings.user)) return { value: settings.user, source: "user" };
|
|
49813
|
+
if (settings.roleValues.includes("yes")) return { value: "yes", source: "role" };
|
|
49814
|
+
if (yesOrNo(settings.everyone)) return { value: settings.everyone, source: "everyone-default" };
|
|
49815
|
+
return null;
|
|
49816
|
+
}
|
|
49583
49817
|
function resolvePermission(subject, capability, targetScopeKind, targetScopeId, world) {
|
|
49584
49818
|
if (subject.instanceAdmin) {
|
|
49585
49819
|
return { value: "yes", source: "instance-admin" };
|
|
49586
49820
|
}
|
|
49587
|
-
const chain = ancestorChain(targetScopeKind, targetScopeId, world);
|
|
49821
|
+
const chain = ancestorChain(capability, targetScopeKind, targetScopeId, world);
|
|
49588
49822
|
for (const scope of chain) {
|
|
49589
49823
|
const settings = settingsAtScope(subject, capability, scope, world);
|
|
49590
|
-
const decision = decideScope(settings);
|
|
49824
|
+
const decision = decideScope(settings, scope.kind);
|
|
49591
49825
|
if (decision) {
|
|
49592
49826
|
return {
|
|
49593
49827
|
value: decision.value,
|
|
@@ -49606,7 +49840,21 @@ function allScopes(world) {
|
|
|
49606
49840
|
...projectIds.map((id) => ({ kind: "project", id }))
|
|
49607
49841
|
];
|
|
49608
49842
|
}
|
|
49843
|
+
function namedSubsystemScopes(subject, world) {
|
|
49844
|
+
const ids = [
|
|
49845
|
+
...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.capability === SUBSYSTEM_CAPABILITY && a.scopeId).map((a) => a.scopeId),
|
|
49846
|
+
...subject.roleBindings.filter((b) => b.scopeKind === "subsystem" && b.scopeId).map((b) => b.scopeId)
|
|
49847
|
+
];
|
|
49848
|
+
return [...new Set(ids)].filter((id) => parseSubsystemScope(id) !== null);
|
|
49849
|
+
}
|
|
49850
|
+
function judgedScopes(subject, capability, world) {
|
|
49851
|
+
const subsystems = capability === SUBSYSTEM_CAPABILITY ? namedSubsystemScopes(subject, world).map((id) => ({ kind: "subsystem", id })) : [];
|
|
49852
|
+
return [...allScopes(world), ...subsystems];
|
|
49853
|
+
}
|
|
49609
49854
|
function ancestorUnitsOf(scope, world) {
|
|
49855
|
+
if (scope.kind === "subsystem") {
|
|
49856
|
+
return familyRootUnits(parseSubsystemScope(scope.id)?.projectId ?? "", world).map((s) => s.id);
|
|
49857
|
+
}
|
|
49610
49858
|
if (scope.kind === "project") return familyRootUnits(scope.id, world).map((s) => s.id);
|
|
49611
49859
|
return unitChain(scope.id, world).map((s) => s.id).filter((id) => id !== scope.id);
|
|
49612
49860
|
}
|
|
@@ -49621,7 +49869,7 @@ function resolveVisibleScopes(subject, capability, world) {
|
|
|
49621
49869
|
}
|
|
49622
49870
|
const actionable = /* @__PURE__ */ new Map();
|
|
49623
49871
|
const context = /* @__PURE__ */ new Map();
|
|
49624
|
-
for (const scope of
|
|
49872
|
+
for (const scope of judgedScopes(subject, capability, world)) {
|
|
49625
49873
|
const effective = resolvePermission(subject, capability, scope.kind, scope.id, world);
|
|
49626
49874
|
if (effective.value === "yes" || effective.value === "approval") {
|
|
49627
49875
|
actionable.set(scopeKey(scope.kind, scope.id), {
|
|
@@ -49630,6 +49878,15 @@ function resolveVisibleScopes(subject, capability, world) {
|
|
|
49630
49878
|
value: effective.value,
|
|
49631
49879
|
context: false
|
|
49632
49880
|
});
|
|
49881
|
+
const project2 = scope.kind === "subsystem" ? parseSubsystemScope(scope.id)?.projectId : void 0;
|
|
49882
|
+
if (project2 !== void 0 && !context.has(scopeKey("project", project2))) {
|
|
49883
|
+
context.set(scopeKey("project", project2), {
|
|
49884
|
+
scopeKind: "project",
|
|
49885
|
+
scopeId: project2,
|
|
49886
|
+
value: resolvePermission(subject, capability, "project", project2, world).value,
|
|
49887
|
+
context: true
|
|
49888
|
+
});
|
|
49889
|
+
}
|
|
49633
49890
|
for (const unitId of ancestorUnitsOf(scope, world)) {
|
|
49634
49891
|
const key = scopeKey("unit", unitId);
|
|
49635
49892
|
if (context.has(key)) continue;
|
|
@@ -49645,6 +49902,26 @@ function resolveVisibleScopes(subject, capability, world) {
|
|
|
49645
49902
|
const contextOnly = [...context.entries()].filter(([key]) => !actionable.has(key)).map(([, scope]) => scope);
|
|
49646
49903
|
return [...actionable.values(), ...contextOnly];
|
|
49647
49904
|
}
|
|
49905
|
+
function resolveSubsystemReach(subject, projectId, world) {
|
|
49906
|
+
if (subject.instanceAdmin) return [];
|
|
49907
|
+
const prefix = `${projectId}${SUBSYSTEM_SCOPE_SEPARATOR}`;
|
|
49908
|
+
const applies = (a) => a.subjectKind === "everyone" || a.subjectKind === "user" && subjectIdMatches(subject, a.subjectId);
|
|
49909
|
+
const scopeIds = [
|
|
49910
|
+
...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.scopeId?.startsWith(prefix) && applies(a)).map((a) => a.scopeId),
|
|
49911
|
+
...subject.roleBindings.filter((b) => b.scopeKind === "subsystem" && b.scopeId?.startsWith(prefix)).map((b) => b.scopeId)
|
|
49912
|
+
];
|
|
49913
|
+
const reach2 = [];
|
|
49914
|
+
for (const scopeId of new Set(scopeIds)) {
|
|
49915
|
+
const parsed = parseSubsystemScope(scopeId);
|
|
49916
|
+
if (!parsed || parsed.projectId !== projectId) continue;
|
|
49917
|
+
reach2.push({
|
|
49918
|
+
projectId,
|
|
49919
|
+
subsystemId: parsed.subsystemId,
|
|
49920
|
+
permission: resolvePermission(subject, SUBSYSTEM_CAPABILITY, "subsystem", scopeId, world)
|
|
49921
|
+
});
|
|
49922
|
+
}
|
|
49923
|
+
return reach2;
|
|
49924
|
+
}
|
|
49648
49925
|
|
|
49649
49926
|
// src/server/authorization.ts
|
|
49650
49927
|
function mergeRoles(stored) {
|
|
@@ -49665,15 +49942,20 @@ function gatherPermissionWorld(dataDir) {
|
|
|
49665
49942
|
return gatherWorld(dataDir);
|
|
49666
49943
|
}
|
|
49667
49944
|
function resolveFamilyReach(dataDir, principal, projectIds) {
|
|
49668
|
-
const reach2 = { readable: [], writable: [] };
|
|
49945
|
+
const reach2 = { readable: [], writable: [], subsystems: [] };
|
|
49669
49946
|
if (!principal.authenticated || !principal.permissionSubject) return reach2;
|
|
49670
49947
|
const world = gatherPermissionWorld(dataDir);
|
|
49671
49948
|
for (const id of projectIds) {
|
|
49672
49949
|
if (resolvePermission(principal.permissionSubject, "project:read", "project", id, world).value === "yes") reach2.readable.push(id);
|
|
49673
49950
|
if (resolvePermission(principal.permissionSubject, "project:write", "project", id, world).value === "yes") reach2.writable.push(id);
|
|
49951
|
+
reach2.subsystems.push(...resolveSubsystemReach(principal.permissionSubject, id, world));
|
|
49674
49952
|
}
|
|
49675
49953
|
return reach2;
|
|
49676
49954
|
}
|
|
49955
|
+
function explain2(dataDir, subject, capability, scopeKind, scopeId) {
|
|
49956
|
+
const world = gatherPermissionWorld(dataDir);
|
|
49957
|
+
return resolvePermission(subject, capability, scopeKind, scopeId, world);
|
|
49958
|
+
}
|
|
49677
49959
|
function authorize(dataDir, principal, capability, scopeKind, scopeId) {
|
|
49678
49960
|
if (!principal.authenticated || !principal.permissionSubject) {
|
|
49679
49961
|
return { value: "no", source: "instance-default" };
|
|
@@ -50159,6 +50441,40 @@ function compare5(users, before, beforeId, after, afterId) {
|
|
|
50159
50441
|
}
|
|
50160
50442
|
return rows;
|
|
50161
50443
|
}
|
|
50444
|
+
var subjectOfSetting = (subjectKind, subjectId) => subjectKind === "everyone" ? EVERYONE_ELSE : subjectId ?? "";
|
|
50445
|
+
function carryPlan(users, world, parentId, memberId, moved) {
|
|
50446
|
+
const scopes = new Set(moved.map((s) => `${parentId}/${s}`));
|
|
50447
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
50448
|
+
for (const a of world.assignments) {
|
|
50449
|
+
if (a.scopeKind !== "subsystem" || !scopes.has(a.scopeId ?? "") || a.capability !== "project:write") continue;
|
|
50450
|
+
if (a.value !== "yes" && a.value !== "no") continue;
|
|
50451
|
+
const subject = subjectOfSetting(a.subjectKind, a.subjectId);
|
|
50452
|
+
const held = bySubject.get(subject) ?? { scopes: [], values: [] };
|
|
50453
|
+
held.scopes.push(a.scopeId);
|
|
50454
|
+
held.values.push(a.value);
|
|
50455
|
+
bySubject.set(subject, held);
|
|
50456
|
+
}
|
|
50457
|
+
const own = new Set(world.assignments.filter((a) => a.scopeKind === "project" && a.scopeId === memberId && a.capability === "project:write").map((a) => subjectOfSetting(a.subjectKind, a.subjectId)));
|
|
50458
|
+
const rules = [];
|
|
50459
|
+
for (const [subject, held] of bySubject) {
|
|
50460
|
+
if (own.has(subject)) continue;
|
|
50461
|
+
rules.push({
|
|
50462
|
+
from: [...held.scopes].sort(),
|
|
50463
|
+
to: memberId,
|
|
50464
|
+
subject,
|
|
50465
|
+
kind: "assignment",
|
|
50466
|
+
value: held.values.includes("no") ? "no" : "yes",
|
|
50467
|
+
note: held.scopes.length > 1 ? "most restrictive wins" : "carried"
|
|
50468
|
+
});
|
|
50469
|
+
}
|
|
50470
|
+
for (const user of users) {
|
|
50471
|
+
for (const b of user.roleBindings ?? []) {
|
|
50472
|
+
if (b.scopeKind !== "subsystem" || !scopes.has(b.scopeId ?? "")) continue;
|
|
50473
|
+
rules.push({ from: [b.scopeId], to: memberId, subject: user.subject.userId || user.id, kind: "role", value: b.roleId, note: "not carried (can only narrow)" });
|
|
50474
|
+
}
|
|
50475
|
+
}
|
|
50476
|
+
return rules;
|
|
50477
|
+
}
|
|
50162
50478
|
function narrowing(key, members) {
|
|
50163
50479
|
if (!key.projects.some((e) => e.includes("::"))) {
|
|
50164
50480
|
return null;
|
|
@@ -50341,6 +50657,7 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
|
|
|
50341
50657
|
// src/server/members.ts
|
|
50342
50658
|
init_fs();
|
|
50343
50659
|
init_project();
|
|
50660
|
+
var EVERYONE = "everyone";
|
|
50344
50661
|
var UPGRADE_AREAS = ["projects.json", "auth/credentials.json"];
|
|
50345
50662
|
var UPGRADE_VERB = "member-upgrade";
|
|
50346
50663
|
var OPERATOR = { userId: "operator:host-doctor", kind: "service", issuer: "local" };
|
|
@@ -50536,7 +50853,7 @@ function recoverData(dataDir, fix) {
|
|
|
50536
50853
|
return found;
|
|
50537
50854
|
}
|
|
50538
50855
|
var ROOT_RENAME_REFUSED = "renaming a hosted family root's id is not supported; a member can be renamed, or the root recreated";
|
|
50539
|
-
function screen(dataDir, binding, tool, alias, path93) {
|
|
50856
|
+
function screen(dataDir, binding, tool, alias, path93, newId) {
|
|
50540
50857
|
if (path93 !== void 0 && (tool === "sdd_add_member" || tool === "sdd_attach_member")) {
|
|
50541
50858
|
const storage = parseMemberSource(path93).storage;
|
|
50542
50859
|
if (storage === "path" || storage === "git") {
|
|
@@ -50554,7 +50871,10 @@ function screen(dataDir, binding, tool, alias, path93) {
|
|
|
50554
50871
|
const users = listUsers(dataDir);
|
|
50555
50872
|
const incoming = standIn(binding.projectId, alias, path93);
|
|
50556
50873
|
const after = relink(world, [{ projectId: incoming, parentProjectId: binding.projectId }], [], [], []);
|
|
50557
|
-
|
|
50874
|
+
const reachChanges = compare5(users, world, null, after, incoming).filter((r) => !r.equal);
|
|
50875
|
+
if (tool !== "sdd_promote_member" && tool !== "sdd_externalize_subsystem") return { reachChanges };
|
|
50876
|
+
const moved = tool === "sdd_externalize_subsystem" ? [alias] : partSubsystemIds(dataDir, binding.projectId, alias) ?? [];
|
|
50877
|
+
return { reachChanges, carried: carryPlan(users, world, binding.projectId, newId || alias, moved) };
|
|
50558
50878
|
}
|
|
50559
50879
|
function screenDemote(dataDir, binding, alias) {
|
|
50560
50880
|
const declared = planMemberRecords(dataDir, binding.projectId).find((d) => d.record.parentProjectId === binding.projectId && d.qualifier.endsWith(`${SUBPROJECT_SEPARATOR}${alias}`) && d.action !== "unreadable");
|
|
@@ -50589,7 +50909,8 @@ function reconcile(dataDir, principal, projectId, touched) {
|
|
|
50589
50909
|
setProjectRecordStatus(dataDir, r.id, "disabled");
|
|
50590
50910
|
changes.push({ kind: "departed", id: r.id, detail: { parent: r.parentProjectId, memberPath: r.memberPath } });
|
|
50591
50911
|
}
|
|
50592
|
-
const
|
|
50912
|
+
const carried = carrySubsystemRules(dataDir, listChangedLinks(changes.filter((c) => c.kind === "registered" || c.kind === "returned")));
|
|
50913
|
+
const reachChanges = [...reachOf2(users, world, changes, listAssignments(dataDir)), ...carried];
|
|
50593
50914
|
const ids = [.../* @__PURE__ */ new Set([...touched, ...changes.map((c) => c.id)])];
|
|
50594
50915
|
for (const id of ids) auditTouched(dataDir, principal, projectId, id, changes.find((c) => c.id === id), reachChanges);
|
|
50595
50916
|
const commit3 = publishFamily(dataDir, ids, `wairon: reconcile the family of ${projectId} (${ids.join(", ")})`);
|
|
@@ -50612,16 +50933,28 @@ function rekey(dataDir, world, users, m) {
|
|
|
50612
50933
|
if (refused2) {
|
|
50613
50934
|
return { kind: "registered", id: newId, detail: { parent: m.record.parentProjectId, memberPath: m.record.memberPath, rekeyRefused: oldId } };
|
|
50614
50935
|
}
|
|
50615
|
-
const
|
|
50616
|
-
|
|
50617
|
-
|
|
50936
|
+
const subsystemRemap = subsystemScopesOf(world, users, oldId).map((scopeId) => ({ oldId: scopeId, newId: `${newId}${scopeId.slice(oldId.length)}` }));
|
|
50937
|
+
const moved = {
|
|
50938
|
+
assignments: world.assignments.filter((a) => a.scopeKind === "project" && a.scopeId === oldId).length,
|
|
50939
|
+
subsystemRules: subsystemRemap.map((r) => `${r.oldId} -> ${r.newId}`)
|
|
50940
|
+
};
|
|
50941
|
+
remapScope(dataDir, [{ oldId, newId }, ...subsystemRemap]);
|
|
50942
|
+
remapUnitReferences(dataDir, [{ oldId, newId }, ...subsystemRemap], []);
|
|
50618
50943
|
const keys = listCredentials(dataDir, oldId).filter((k) => k.projects.includes(oldId) && !k.revokedAt);
|
|
50619
50944
|
for (const k of keys) renarrowCredential(dataDir, k.id, [...new Set(k.projects.map((e) => e === oldId ? newId : e))]);
|
|
50620
50945
|
removeProjectRecord(dataDir, oldId);
|
|
50621
50946
|
return { kind: "renamed", id: newId, previousId: oldId, detail: { ...moved, keys: keys.map((k) => k.id) } };
|
|
50622
50947
|
}
|
|
50948
|
+
function subsystemScopesOf(world, users, id) {
|
|
50949
|
+
const prefix = `${id}/`;
|
|
50950
|
+
const ids = [
|
|
50951
|
+
...world.assignments.filter((a) => a.scopeKind === "subsystem" && a.scopeId?.startsWith(prefix)).map((a) => a.scopeId),
|
|
50952
|
+
...users.flatMap((u) => (u.roleBindings ?? []).filter((b) => b.scopeKind === "subsystem" && b.scopeId?.startsWith(prefix)).map((b) => b.scopeId))
|
|
50953
|
+
];
|
|
50954
|
+
return [...new Set(ids)];
|
|
50955
|
+
}
|
|
50623
50956
|
function hasOwnSettings(world, users, id) {
|
|
50624
|
-
return world.assignments.some((a) => a.scopeKind === "project" && a.scopeId === id) || users.some((u) => (u.roleBindings ?? []).some((b) => b.scopeKind === "project" && b.scopeId === id));
|
|
50957
|
+
return world.assignments.some((a) => a.scopeKind === "project" && a.scopeId === id) || users.some((u) => (u.roleBindings ?? []).some((b) => b.scopeKind === "project" && b.scopeId === id)) || subsystemScopesOf(world, users, id).length > 0;
|
|
50625
50958
|
}
|
|
50626
50959
|
function departedRecords(family, declared, changes) {
|
|
50627
50960
|
const live = new Set(declared.filter((d) => d.action !== "unreadable").map((d) => d.record.id));
|
|
@@ -50629,16 +50962,52 @@ function departedRecords(family, declared, changes) {
|
|
|
50629
50962
|
const rekeyed = new Set(changes.filter((c) => c.kind === "renamed").map((c) => c.previousId));
|
|
50630
50963
|
return family.filter((r) => r.parentProjectId !== void 0 && r.status === "active" && !live.has(r.id) && !rekeyed.has(r.id) && !held.includes(`${r.parentProjectId}/${r.memberPath}`));
|
|
50631
50964
|
}
|
|
50632
|
-
function reachOf2(users, world, changes) {
|
|
50965
|
+
function reachOf2(users, world, changes, assignmentsAfter) {
|
|
50633
50966
|
const links = listChangedLinks(changes);
|
|
50634
50967
|
const departed = changes.filter((c) => c.kind === "departed" || c.kind === "retired").map((c) => c.id);
|
|
50635
|
-
const after = relink(world, links, departed, [], []);
|
|
50968
|
+
const after = { ...relink(world, links, departed, [], []), assignments: assignmentsAfter };
|
|
50636
50969
|
return changes.filter((c) => c.kind !== "renamed").flatMap((c) => {
|
|
50637
50970
|
const before = c.kind === "registered" || c.kind === "returned" ? null : c.id;
|
|
50638
50971
|
const now = c.kind === "departed" ? null : c.kind === "retired" ? String(c.detail.parent ?? "") || null : c.id;
|
|
50639
50972
|
return compare5(users, world, before, after, now).filter((r) => !r.equal).map((r) => ({ ...r, memberId: c.id }));
|
|
50640
50973
|
});
|
|
50641
50974
|
}
|
|
50975
|
+
function carrySubsystemRules(dataDir, members) {
|
|
50976
|
+
const rows = [];
|
|
50977
|
+
for (const { projectId: memberId, parentProjectId: parentId } of members) {
|
|
50978
|
+
const parentHolds = declaredSubsystemIds(dataDir, parentId) ?? [];
|
|
50979
|
+
const moved = (declaredSubsystemIds(dataDir, memberId) ?? []).filter((s) => !parentHolds.includes(s));
|
|
50980
|
+
if (moved.length === 0) continue;
|
|
50981
|
+
const plan7 = carryPlan(listUsers(dataDir), gatherPermissionWorld(dataDir), parentId, memberId, moved);
|
|
50982
|
+
for (const rule of plan7.filter((r) => r.kind === "assignment")) {
|
|
50983
|
+
const everyone = rule.subject === EVERYONE;
|
|
50984
|
+
setAssignment(dataDir, {
|
|
50985
|
+
id: "",
|
|
50986
|
+
subjectKind: everyone ? "everyone" : "user",
|
|
50987
|
+
...everyone ? {} : { subjectId: rule.subject },
|
|
50988
|
+
scopeKind: "project",
|
|
50989
|
+
scopeId: memberId,
|
|
50990
|
+
capability: "project:write",
|
|
50991
|
+
value: rule.value,
|
|
50992
|
+
createdAt: ""
|
|
50993
|
+
});
|
|
50994
|
+
rows.push(carriedRow(rule));
|
|
50995
|
+
}
|
|
50996
|
+
}
|
|
50997
|
+
return rows;
|
|
50998
|
+
}
|
|
50999
|
+
function carriedRow(rule) {
|
|
51000
|
+
const source = rule.subject === EVERYONE ? "everyone-default" : "user";
|
|
51001
|
+
const value = rule.value;
|
|
51002
|
+
return {
|
|
51003
|
+
memberId: rule.to,
|
|
51004
|
+
subjectId: rule.subject,
|
|
51005
|
+
capability: "project:write",
|
|
51006
|
+
before: { value, source, decidedScopeKind: "subsystem", decidedScopeId: rule.from.join(", ") },
|
|
51007
|
+
after: { value, source, decidedScopeKind: "project", decidedScopeId: rule.to },
|
|
51008
|
+
equal: true
|
|
51009
|
+
};
|
|
51010
|
+
}
|
|
50642
51011
|
function listChangedLinks(changes) {
|
|
50643
51012
|
return changes.filter((c) => c.kind === "registered" || c.kind === "returned" || c.kind === "relocated").map((c) => ({ projectId: c.id, parentProjectId: String(c.detail.parent) }));
|
|
50644
51013
|
}
|
|
@@ -52207,7 +52576,17 @@ function generateDiagram(cfg, credential, project2, format) {
|
|
|
52207
52576
|
requireAdmin(credential);
|
|
52208
52577
|
const root = existingProjectRoot(cfg.dataDir, project2);
|
|
52209
52578
|
if (!root) throw new Error(`Unknown project "${project2}".`);
|
|
52210
|
-
|
|
52579
|
+
const relations = format === "canvas" ? instanceRelations(cfg, project2, root) : void 0;
|
|
52580
|
+
return runWithProjectRoot(root, () => renderDiagram(format, relations));
|
|
52581
|
+
}
|
|
52582
|
+
function instanceRelations(cfg, project2, root) {
|
|
52583
|
+
const familyRoot = listFamilyRecords(cfg.dataDir, project2)[0];
|
|
52584
|
+
const reach2 = {
|
|
52585
|
+
topRoot: familyRoot?.rootPath || root,
|
|
52586
|
+
parentReach: true,
|
|
52587
|
+
hostedLookup: (id) => existingProjectRoot(cfg.dataDir, id)
|
|
52588
|
+
};
|
|
52589
|
+
return runWithProjectBinding(root, reach2, () => familyRelations2());
|
|
52211
52590
|
}
|
|
52212
52591
|
function downloadDiagram(cfg, credential, project2, format) {
|
|
52213
52592
|
return generateDiagram(cfg, credential, project2, format);
|
|
@@ -52756,6 +53135,7 @@ function setAssignment2(cfg, credential, assignment) {
|
|
|
52756
53135
|
"the instance-admin marker ('*'@instance) is reserved to the built-in admin account"
|
|
52757
53136
|
);
|
|
52758
53137
|
}
|
|
53138
|
+
refuseUnfitSubsystemAssignment(assignment);
|
|
52759
53139
|
requireAdminOverScope(
|
|
52760
53140
|
cfg,
|
|
52761
53141
|
principal,
|
|
@@ -52763,8 +53143,9 @@ function setAssignment2(cfg, credential, assignment) {
|
|
|
52763
53143
|
assignment.scopeId ?? "",
|
|
52764
53144
|
"setting an assignment requires project:admin over its scope"
|
|
52765
53145
|
);
|
|
53146
|
+
const { scopeNote: _label, ...unlabelled } = assignment;
|
|
52766
53147
|
const stored = setAssignment(cfg.dataDir, {
|
|
52767
|
-
...
|
|
53148
|
+
...unlabelled,
|
|
52768
53149
|
// Canonical grid key: user-kind subjects store the subject's userId, so
|
|
52769
53150
|
// permission rules' primary match (not the legacy-alias path) serves them.
|
|
52770
53151
|
...assignment.subjectKind === "user" && assignment.subjectId ? { subjectId: canonicalSubjectId(cfg, assignment.subjectId) } : {},
|
|
@@ -52773,6 +53154,27 @@ function setAssignment2(cfg, credential, assignment) {
|
|
|
52773
53154
|
tryAppendAudit(cfg, buildAuditEvent(principal, "permission.set", stored.id));
|
|
52774
53155
|
return stored;
|
|
52775
53156
|
}
|
|
53157
|
+
var SUBSYSTEM_VALUES = /* @__PURE__ */ new Set(["yes", "no", "inherit"]);
|
|
53158
|
+
function refuseUnfitSubsystemAssignment(assignment) {
|
|
53159
|
+
if (assignment.scopeKind !== "subsystem") return;
|
|
53160
|
+
const fits = String(assignment.capability) === "project:write" && SUBSYSTEM_VALUES.has(String(assignment.value)) && parseSubsystemScope(assignment.scopeId ?? "") !== null;
|
|
53161
|
+
if (!fits) {
|
|
53162
|
+
throw new ForbiddenError(
|
|
53163
|
+
`a subsystem scope carries only project:write, yes or no (or inherit), at <projectId>/<subsystemId> \u2014 got ${assignment.capability}=${assignment.value} at "${assignment.scopeId ?? ""}"`
|
|
53164
|
+
);
|
|
53165
|
+
}
|
|
53166
|
+
}
|
|
53167
|
+
function refuseUnfitSubsystemBinding(scopeKind, scopeId) {
|
|
53168
|
+
if (scopeKind === "subsystem" && parseSubsystemScope(scopeId ?? "") === null) {
|
|
53169
|
+
throw new ForbiddenError(`a subsystem scope id is <projectId>/<subsystemId> \u2014 got "${scopeId ?? ""}"`);
|
|
53170
|
+
}
|
|
53171
|
+
}
|
|
53172
|
+
function subsystemMissing(cfg, scopeId, cache2) {
|
|
53173
|
+
const parsed = parseSubsystemScope(scopeId);
|
|
53174
|
+
if (!parsed) return true;
|
|
53175
|
+
if (!cache2.has(parsed.projectId)) cache2.set(parsed.projectId, declaredSubsystemIds(cfg.dataDir, parsed.projectId));
|
|
53176
|
+
return !(cache2.get(parsed.projectId) ?? []).includes(parsed.subsystemId);
|
|
53177
|
+
}
|
|
52776
53178
|
function removeAssignment2(cfg, credential, assignmentId) {
|
|
52777
53179
|
const principal = requirePrincipal3(cfg, credential);
|
|
52778
53180
|
const existing = getAssignment(cfg.dataDir, assignmentId);
|
|
@@ -52801,13 +53203,15 @@ function listAssignments2(cfg, credential, scopeKind, scopeId, subjectKind, subj
|
|
|
52801
53203
|
const subjectIds = subjectKind === "user" && subjectId ? [.../* @__PURE__ */ new Set([subjectId, canonicalSubjectId(cfg, subjectId)])] : void 0;
|
|
52802
53204
|
const assignments = subjectIds ? subjectIds.flatMap((id) => listAssignments(cfg.dataDir, void 0, subjectKind, id)) : listAssignments(cfg.dataDir, void 0, subjectKind, subjectId);
|
|
52803
53205
|
const seen = /* @__PURE__ */ new Set();
|
|
52804
|
-
|
|
53206
|
+
const listed = assignments.filter((a) => {
|
|
52805
53207
|
if (seen.has(a.id)) return false;
|
|
52806
53208
|
seen.add(a.id);
|
|
52807
53209
|
if (scopeKind !== void 0 && a.scopeKind !== scopeKind) return false;
|
|
52808
53210
|
if (scopeId !== void 0 && (a.scopeId ?? "") !== scopeId) return false;
|
|
52809
53211
|
return true;
|
|
52810
53212
|
});
|
|
53213
|
+
const trees = /* @__PURE__ */ new Map();
|
|
53214
|
+
return listed.map((a) => a.scopeKind === "subsystem" && subsystemMissing(cfg, a.scopeId ?? "", trees) ? { ...a, scopeNote: SUBSYSTEM_NOT_FOUND } : a);
|
|
52811
53215
|
}
|
|
52812
53216
|
function bindingScope(scopeKind, scopeId) {
|
|
52813
53217
|
return { kind: scopeKind ?? "instance", id: scopeId ?? "" };
|
|
@@ -52817,6 +53221,7 @@ function sameBinding(a, b) {
|
|
|
52817
53221
|
}
|
|
52818
53222
|
function bindRole(cfg, credential, userId, roleId, scopeKind, scopeId) {
|
|
52819
53223
|
const principal = requirePrincipal3(cfg, credential);
|
|
53224
|
+
refuseUnfitSubsystemBinding(scopeKind, scopeId);
|
|
52820
53225
|
const scope = bindingScope(scopeKind, scopeId);
|
|
52821
53226
|
requireAdminOverScope(cfg, principal, scope.kind, scope.id, "binding a role requires project:admin over the scope");
|
|
52822
53227
|
const user = getUserById(cfg.dataDir, userId);
|
|
@@ -52852,6 +53257,27 @@ function unbindRole(cfg, credential, userId, roleId, scopeKind, scopeId) {
|
|
|
52852
53257
|
tryAppendAudit(cfg, buildAuditEvent(principal, "role.unbind", `${userId}:${roleId}`));
|
|
52853
53258
|
return stored;
|
|
52854
53259
|
}
|
|
53260
|
+
function subjectOfUser(user) {
|
|
53261
|
+
const subjectId = user.subject.userId || user.id;
|
|
53262
|
+
const aliases = [user.id, user.subject.userId].filter((id) => !!id && id !== subjectId);
|
|
53263
|
+
return {
|
|
53264
|
+
subjectId,
|
|
53265
|
+
...aliases.length ? { aliasSubjectIds: [...new Set(aliases)] } : {},
|
|
53266
|
+
roleBindings: user.roleBindings ?? [],
|
|
53267
|
+
instanceAdmin: false
|
|
53268
|
+
};
|
|
53269
|
+
}
|
|
53270
|
+
function explain3(cfg, credential, userId, capability, scopeKind, scopeId) {
|
|
53271
|
+
const principal = requirePrincipal3(cfg, credential);
|
|
53272
|
+
requireAdminOverScope(cfg, principal, scopeKind, scopeId, "explaining a permission requires project:admin over its scope");
|
|
53273
|
+
const user = findUserByRecordOrSubjectId(cfg.dataDir, userId);
|
|
53274
|
+
if (!user) throw new Error(`User "${userId}" not found.`);
|
|
53275
|
+
const explained = explain2(cfg.dataDir, subjectOfUser(user), capability, scopeKind, scopeId);
|
|
53276
|
+
if (scopeKind === "subsystem" && subsystemMissing(cfg, scopeId, /* @__PURE__ */ new Map())) {
|
|
53277
|
+
return { ...explained, scopeNote: SUBSYSTEM_NOT_FOUND };
|
|
53278
|
+
}
|
|
53279
|
+
return explained;
|
|
53280
|
+
}
|
|
52855
53281
|
|
|
52856
53282
|
// src/server/identity.ts
|
|
52857
53283
|
var crypto19 = __toESM(require("crypto"));
|
|
@@ -56118,6 +56544,7 @@ function lockTargetOf(cfg, req) {
|
|
|
56118
56544
|
function lifecycleAction(cfg, credential, projectId, opts) {
|
|
56119
56545
|
const principal = requirePrincipal7(cfg, credential);
|
|
56120
56546
|
const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
|
|
56547
|
+
if (effective.value !== "no") refuseDeniedSubsystems(cfg, principal, projectId);
|
|
56121
56548
|
switch (effective.value) {
|
|
56122
56549
|
case "yes": {
|
|
56123
56550
|
const outcome = opts.execute(hostedApprover(principal.subject));
|
|
@@ -56149,6 +56576,13 @@ function lifecycleAction(cfg, credential, projectId, opts) {
|
|
|
56149
56576
|
}
|
|
56150
56577
|
}
|
|
56151
56578
|
}
|
|
56579
|
+
function refuseDeniedSubsystems(cfg, principal, projectId) {
|
|
56580
|
+
const denied = resolveFamilyReach(cfg.dataDir, principal, [projectId]).subsystems.filter((s) => s.projectId === projectId && s.permission.value === "no");
|
|
56581
|
+
if (denied.length === 0) return;
|
|
56582
|
+
throw new ForbiddenError(
|
|
56583
|
+
"a lock signs the whole tree, and the caller may not change these subsystems: " + denied.map((s) => `${s.subsystemId} (decided by ${describeDecision(s.permission)})`).join(", ")
|
|
56584
|
+
);
|
|
56585
|
+
}
|
|
56152
56586
|
function getApprovalStatus(cfg, credential, requestId) {
|
|
56153
56587
|
const principal = requirePrincipal7(cfg, credential);
|
|
56154
56588
|
expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -57431,6 +57865,9 @@ function bindRole2(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
|
|
|
57431
57865
|
function unbindRole2(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
|
|
57432
57866
|
return unbindRole(cfg, sessionId, userId, roleId, scopeKind, scopeId);
|
|
57433
57867
|
}
|
|
57868
|
+
function explainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId) {
|
|
57869
|
+
return explain3(cfg, sessionId, userId, capability, scopeKind, scopeId);
|
|
57870
|
+
}
|
|
57434
57871
|
|
|
57435
57872
|
// src/server/webproject.ts
|
|
57436
57873
|
function listProjects2(cfg, sessionId) {
|
|
@@ -58137,7 +58574,9 @@ function getWebProjectCanvas(cfg, sessionId, projectId) {
|
|
|
58137
58574
|
if (!root) {
|
|
58138
58575
|
throw new ForbiddenError("project not authorized or unknown");
|
|
58139
58576
|
}
|
|
58140
|
-
|
|
58577
|
+
const listed = listProjects2(cfg, sessionId);
|
|
58578
|
+
const relations = runWithProjectBinding(root, readReach(cfg, projectId, root, listed), () => familyRelations2());
|
|
58579
|
+
return runWithProjectRoot(root, () => renderDiagram("canvas", relations));
|
|
58141
58580
|
}
|
|
58142
58581
|
function getWebProjectCanvasModel(cfg, sessionId, projectId) {
|
|
58143
58582
|
const principal = authenticateSession(cfg.dataDir, sessionId);
|
|
@@ -60323,6 +60762,9 @@ function adminUnbindRole(cfg, sessionId, userId, roleId, scopeKind, scopeId) {
|
|
|
60323
60762
|
function adminListAssignments(cfg, sessionId, scopeKind, scopeId, subjectKind, subjectId) {
|
|
60324
60763
|
return listAssignments3(cfg, sessionId, scopeKind, scopeId, subjectKind, subjectId);
|
|
60325
60764
|
}
|
|
60765
|
+
function adminExplainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId) {
|
|
60766
|
+
return explainPermission(cfg, sessionId, userId, capability, scopeKind, scopeId ?? "");
|
|
60767
|
+
}
|
|
60326
60768
|
function adminSetAssignment(cfg, sessionId, assignment) {
|
|
60327
60769
|
return setAssignment3(cfg, sessionId, assignment);
|
|
60328
60770
|
}
|
|
@@ -60824,6 +61266,10 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
|
|
|
60824
61266
|
)
|
|
60825
61267
|
});
|
|
60826
61268
|
}
|
|
61269
|
+
if (req.method === "GET" && parts.length === 4 && parts[2] === "permissions" && parts[3] === "explain") {
|
|
61270
|
+
const q2 = (name) => url.searchParams.get(name) ?? "";
|
|
61271
|
+
return sendJson(res, 200, adminExplainPermission(cfg, sessionId, q2("userId"), q2("capability"), q2("scopeKind") || "instance", q2("scopeId") || void 0));
|
|
61272
|
+
}
|
|
60827
61273
|
if (req.method === "POST" && parts.length === 3 && parts[2] === "permissions") {
|
|
60828
61274
|
return sendJson(res, 200, adminSetAssignment(cfg, sessionId, body));
|
|
60829
61275
|
}
|
|
@@ -61209,6 +61655,23 @@ var WRITE_TOOL_PREFIXES = [
|
|
|
61209
61655
|
"sdd_promote_",
|
|
61210
61656
|
"sdd_demote_"
|
|
61211
61657
|
];
|
|
61658
|
+
var SUBSYSTEM_SCOPED_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
61659
|
+
"sdd_add_subsystem",
|
|
61660
|
+
"sdd_add_component",
|
|
61661
|
+
"sdd_add_type",
|
|
61662
|
+
"sdd_define_interface",
|
|
61663
|
+
"sdd_write_narrative",
|
|
61664
|
+
"sdd_set_endpoints",
|
|
61665
|
+
"sdd_set_public_interfaces",
|
|
61666
|
+
"sdd_update_spec",
|
|
61667
|
+
"sdd_delete_spec",
|
|
61668
|
+
"sdd_rename_component",
|
|
61669
|
+
"sdd_rename_method",
|
|
61670
|
+
"sdd_move_methods"
|
|
61671
|
+
]);
|
|
61672
|
+
function isSubsystemScopedWrite(toolName) {
|
|
61673
|
+
return SUBSYSTEM_SCOPED_WRITE_TOOLS.has(toolName);
|
|
61674
|
+
}
|
|
61212
61675
|
var READ_TOOL_PREFIXES = ["sdd_get_", "sdd_validate_"];
|
|
61213
61676
|
var READ_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
61214
61677
|
"listAgents",
|
|
@@ -61244,18 +61707,22 @@ function mcpChangeChannels(body, projectId, response) {
|
|
|
61244
61707
|
if (MUTATING_HOST_TOOLS.has(name)) return [`project:${projectId}`, "projects"];
|
|
61245
61708
|
return [];
|
|
61246
61709
|
}
|
|
61247
|
-
function dataPlanePermissionError(cfg, principal, projectId, body) {
|
|
61710
|
+
function dataPlanePermissionError(cfg, principal, projectId, body, reach2) {
|
|
61248
61711
|
const msg = jsonRpcRequest(body);
|
|
61249
61712
|
if (!msg || msg.method !== "tools/call") return void 0;
|
|
61250
61713
|
const name = msg.params?.name;
|
|
61251
61714
|
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
61252
61715
|
const needed = requiredDataPlaneCapability(name);
|
|
61253
|
-
|
|
61716
|
+
const decided = authorize(cfg.dataDir, principal, needed, "project", projectId);
|
|
61717
|
+
if (decided.value === "yes") return void 0;
|
|
61718
|
+
if (needed === "project:write" && isSubsystemScopedWrite(name) && reach2.subsystems.some((s) => s.projectId === projectId && s.permission.value === "yes")) {
|
|
61719
|
+
return void 0;
|
|
61720
|
+
}
|
|
61254
61721
|
return {
|
|
61255
61722
|
jsonrpc: "2.0",
|
|
61256
61723
|
id: msg.id ?? null,
|
|
61257
61724
|
result: {
|
|
61258
|
-
content: [{ type: "text", text: `Forbidden \u2014 permission ${needed} required for ${name}` }],
|
|
61725
|
+
content: [{ type: "text", text: `Forbidden \u2014 permission ${needed} required for ${name}, decided at ${describeDecision(decided)}` }],
|
|
61259
61726
|
isError: true
|
|
61260
61727
|
}
|
|
61261
61728
|
};
|
|
@@ -61431,7 +61898,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
61431
61898
|
for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
|
|
61432
61899
|
return;
|
|
61433
61900
|
}
|
|
61434
|
-
const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
|
|
61901
|
+
const permissionError = dataPlanePermissionError(cfg, principal, projectId, body, reach2.family);
|
|
61435
61902
|
if (permissionError !== void 0) {
|
|
61436
61903
|
sendJson(res, 200, permissionError);
|
|
61437
61904
|
auditToolCall(cfg.dataDir, principal, binding, body, deriveMcpOutcome(permissionError));
|
|
@@ -61471,7 +61938,11 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
|
|
|
61471
61938
|
for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
|
|
61472
61939
|
});
|
|
61473
61940
|
}
|
|
61474
|
-
var MEMBERSHIP_TOOLS = /* @__PURE__ */ new Set(["sdd_attach_member", "sdd_adopt_member", "sdd_detach_member", "sdd_rename_project", "sdd_promote_member", "sdd_demote_member", "sdd_add_member"]);
|
|
61941
|
+
var MEMBERSHIP_TOOLS = /* @__PURE__ */ new Set(["sdd_attach_member", "sdd_adopt_member", "sdd_detach_member", "sdd_rename_project", "sdd_promote_member", "sdd_demote_member", "sdd_add_member", "sdd_externalize_subsystem"]);
|
|
61942
|
+
function isMembershipCall(call) {
|
|
61943
|
+
if (call.name === "sdd_externalize_subsystem") return call.args.as === "project";
|
|
61944
|
+
return MEMBERSHIP_TOOLS.has(call.name);
|
|
61945
|
+
}
|
|
61475
61946
|
var FAMILY_SHAPE_TOOLS = /* @__PURE__ */ new Set([
|
|
61476
61947
|
"sdd_add_member",
|
|
61477
61948
|
"sdd_move_member",
|
|
@@ -61493,7 +61964,7 @@ function reachLines(rows) {
|
|
|
61493
61964
|
}
|
|
61494
61965
|
function serveMembershipTool(cfg, principal, binding, body) {
|
|
61495
61966
|
const call = toolCall(body);
|
|
61496
|
-
if (!call || !
|
|
61967
|
+
if (!call || !isMembershipCall(call)) return {};
|
|
61497
61968
|
const apply4 = call.args.dryRun !== true;
|
|
61498
61969
|
const answer = (result) => ({ response: { jsonrpc: "2.0", id: call.id, result } });
|
|
61499
61970
|
try {
|
|
@@ -61503,9 +61974,10 @@ function serveMembershipTool(cfg, principal, binding, body) {
|
|
|
61503
61974
|
case "sdd_adopt_member":
|
|
61504
61975
|
return answer(relocationResult("adopt", adopt(cfg.dataDir, principal, binding, String(call.args.alias ?? ""), String(call.args.path ?? ""), apply4), apply4));
|
|
61505
61976
|
default: {
|
|
61506
|
-
const named2 = call.name === "sdd_rename_project" ? String(call.args.project ?? "") : String(call.args.alias ?? "");
|
|
61977
|
+
const named2 = call.name === "sdd_rename_project" ? String(call.args.project ?? "") : call.name === "sdd_externalize_subsystem" ? String(call.args.subsystem ?? "") : String(call.args.alias ?? "");
|
|
61507
61978
|
const located = typeof call.args.source === "string" ? call.args.source : typeof call.args.path === "string" ? call.args.path : void 0;
|
|
61508
|
-
const
|
|
61979
|
+
const newId = call.name === "sdd_promote_member" && typeof call.args.id === "string" && call.args.id ? call.args.id : void 0;
|
|
61980
|
+
const screened = screen(cfg.dataDir, binding, call.name, named2, located, newId);
|
|
61509
61981
|
if (screened.refusal !== void 0) return answer({ content: [{ type: "text", text: screened.refusal }], isError: true });
|
|
61510
61982
|
return { screen: screened };
|
|
61511
61983
|
}
|
|
@@ -61549,6 +62021,10 @@ function withReachListing(result, screened, body) {
|
|
|
61549
62021
|
const lines = reachLines(screened.reachChanges);
|
|
61550
62022
|
r.content.push({ type: "text", text: lines.length > 0 ? `Reach changes \u2014 who gains access through the new parent:
|
|
61551
62023
|
${lines.join("\n")}` : "Reach changes: none \u2014 nobody gains or loses access." });
|
|
62024
|
+
if (!screened.carried) return;
|
|
62025
|
+
const carried = screened.carried.map((c) => `${c.from.join(", ")} -> project ${c.to}: ${c.subject} ` + (c.kind === "role" ? `role ${c.value} \u2014 ${c.note}` : `project:write ${c.value} (${c.note})`));
|
|
62026
|
+
r.content.push({ type: "text", text: carried.length > 0 ? `Subsystem rules carried onto the new project:
|
|
62027
|
+
${carried.join("\n")}` : "Subsystem rules carried onto the new project: none." });
|
|
61552
62028
|
}
|
|
61553
62029
|
function reconcileAfter(cfg, principal, binding, body, response) {
|
|
61554
62030
|
const call = toolCall(body);
|
|
@@ -61591,7 +62067,14 @@ function familyReachOf(cfg, principal, binding) {
|
|
|
61591
62067
|
const topRoot = existingProjectRoot(cfg.dataDir, binding.familyRootId) ?? binding.rootPath;
|
|
61592
62068
|
const parentReach = reach2.readable.includes(binding.familyRootId) && covered(binding.familyRootId);
|
|
61593
62069
|
const unwritableRoots = family.filter((r) => r.rootPath && !isRetiredPart(r) && !(reach2.writable.includes(r.id) && covered(r.id))).map((r) => r.rootPath);
|
|
61594
|
-
|
|
62070
|
+
const roots = new Map(family.filter((r) => r.rootPath && !isRetiredPart(r)).map((r) => [r.id, r.rootPath]));
|
|
62071
|
+
const subsystems = reach2.subsystems.flatMap((s) => {
|
|
62072
|
+
const root = roots.get(s.projectId);
|
|
62073
|
+
if (!root) return [];
|
|
62074
|
+
return [{ root, subsystemId: s.subsystemId, writable: s.permission.value === "yes" && covered(s.projectId), decidedBy: describeDecision(s.permission) }];
|
|
62075
|
+
});
|
|
62076
|
+
const writeReach = { unwritableRoots, subsystems };
|
|
62077
|
+
return { topRoot, parentReach, unwritableRoots, hostedLookup: hostedRecordLookup(cfg, principal), writeReach, family: reach2 };
|
|
61595
62078
|
}
|
|
61596
62079
|
function hostedRecordLookup(cfg, principal) {
|
|
61597
62080
|
return (recordId) => {
|
|
@@ -63133,6 +63616,10 @@ function printMemberUpgrade(upgrade, fix) {
|
|
|
63133
63616
|
var CAPABILITIES = ["project:read", "project:create", "project:write", "project:admin", "approval:decide", "share:create"];
|
|
63134
63617
|
var PERMISSION_VALUES = ["yes", "approval", "no", "inherit"];
|
|
63135
63618
|
function resolveScopeOptions(options) {
|
|
63619
|
+
if (options.subsystem) {
|
|
63620
|
+
if (!options.project) throw new WaironError("`--subsystem <id>` needs `--project <id>`: a subsystem scope is `<project>/<subsystem>`.");
|
|
63621
|
+
return { kind: "subsystem", id: `${options.project}/${options.subsystem}` };
|
|
63622
|
+
}
|
|
63136
63623
|
if (options.project) return { kind: "project", id: options.project };
|
|
63137
63624
|
if (options.unit) return { kind: "unit", id: options.unit };
|
|
63138
63625
|
return { kind: "instance" };
|
|
@@ -63172,7 +63659,7 @@ async function runHostPermission(action, options = {}) {
|
|
|
63172
63659
|
const list2 = listAssignments2(
|
|
63173
63660
|
cfg,
|
|
63174
63661
|
cred,
|
|
63175
|
-
options.project || options.unit || options.instance ? scope.kind : void 0,
|
|
63662
|
+
options.project || options.unit || options.instance || options.subsystem ? scope.kind : void 0,
|
|
63176
63663
|
scope.id,
|
|
63177
63664
|
options.user ? "user" : void 0,
|
|
63178
63665
|
options.user
|
|
@@ -63182,7 +63669,7 @@ async function runHostPermission(action, options = {}) {
|
|
|
63182
63669
|
} else {
|
|
63183
63670
|
for (const a of list2) {
|
|
63184
63671
|
const subject = a.subjectKind === "everyone" ? "everyone" : a.subjectId;
|
|
63185
|
-
logger.info(` ${a.id} ${String(subject).padEnd(20)} ${a.capability.padEnd(16)} ${a.value.padEnd(9)} ${a.scopeKind}${a.scopeId ? ` ${a.scopeId}` : ""}`);
|
|
63672
|
+
logger.info(` ${a.id} ${String(subject).padEnd(20)} ${a.capability.padEnd(16)} ${a.value.padEnd(9)} ${a.scopeKind}${a.scopeId ? ` ${a.scopeId}` : ""}${a.scopeNote ? ` (${a.scopeNote})` : ""}`);
|
|
63186
63673
|
}
|
|
63187
63674
|
}
|
|
63188
63675
|
break;
|
|
@@ -64720,12 +65207,13 @@ hostCmd.command("unit <action>").description("create an organization unit (proje
|
|
|
64720
65207
|
hostCmd.command("doctor").description("migrate a hosted data dir to the permission model (grants \u2192 assignments, owners, unit ids, placements); dry-run without --fix").option("--fix", "apply the migration (REQUIRED at rollout \u2014 legacy users/tokens otherwise resolve to zero permissions)").option("--data-dir <path>", "data root").action(async (opts) => {
|
|
64721
65208
|
await runHostDoctor({ fix: opts.fix, dataDir: opts.dataDir });
|
|
64722
65209
|
});
|
|
64723
|
-
hostCmd.command("permission <action>").description("set | list | remove a permission assignment (the subject\xD7scope\xD7capability grid)").option("--user <userId>", "the subject user id (for set; optional filter for list)").option("--capability <capability>", "project:read | project:create | project:write | project:admin | approval:decide").option("--value <value>", "yes | approval | no | inherit", "yes").option("--project <id>", "anchor the assignment at a project scope").option("--unit <unitId>", "anchor the assignment at an organization-unit scope").option("--instance", "anchor the assignment at the instance scope (the default)").option("--id <assignmentId>", "assignment id (for remove)").option("--data-dir <path>", "data root").action(async (action, opts) => {
|
|
65210
|
+
hostCmd.command("permission <action>").description("set | list | remove a permission assignment (the subject\xD7scope\xD7capability grid)").option("--user <userId>", "the subject user id (for set; optional filter for list)").option("--capability <capability>", "project:read | project:create | project:write | project:admin | approval:decide").option("--value <value>", "yes | approval | no | inherit", "yes").option("--project <id>", "anchor the assignment at a project scope").option("--subsystem <id>", "with --project: anchor at a subsystem of that project (project:write only, yes | no | inherit)").option("--unit <unitId>", "anchor the assignment at an organization-unit scope").option("--instance", "anchor the assignment at the instance scope (the default)").option("--id <assignmentId>", "assignment id (for remove)").option("--data-dir <path>", "data root").action(async (action, opts) => {
|
|
64724
65211
|
await runHostPermission(action, {
|
|
64725
65212
|
user: opts.user,
|
|
64726
65213
|
capability: opts.capability,
|
|
64727
65214
|
value: opts.value,
|
|
64728
65215
|
project: opts.project,
|
|
65216
|
+
subsystem: opts.subsystem,
|
|
64729
65217
|
unit: opts.unit,
|
|
64730
65218
|
instance: opts.instance,
|
|
64731
65219
|
id: opts.id,
|