@cotal-ai/manager 0.153.1 → 0.154.0
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/commands.js +44 -0
- package/dist/commands.js.map +1 -1
- package/dist/manager-service-contract.d.ts +4 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +7 -3
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +15 -6
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +127 -37
- package/dist/manager.js.map +1 -1
- package/dist/resume.d.ts +5 -0
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +14 -1
- package/dist/resume.js.map +1 -1
- package/package.json +12 -12
package/dist/manager.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSy
|
|
|
5
5
|
import { isAbsolute, join, dirname, resolve, sep } from "node:path";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
7
|
import Headless from "@xterm/headless";
|
|
8
|
-
import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, MANAGER_LEASE_TTL_MS, awaitLeaseExpiry, leaseRemainingMs, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, assertValidChannel, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, fenceLifecycleAcl, DeprovisionError, addDeprovisionAccounting, isConcreteChannel, isChannelFamily, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, identityFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, bindIssuedStores, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, oneLine, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, runDriverCaller, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannel, eventChannelPrincipal, resolveService, invokeCommand, digestIdToken, capabilityWithin, grantOverreach, grantParts, isIssuedCaller, issuedSpawnerStanding, withIssuerSession, renderOnceTask, } from "@cotal-ai/core";
|
|
8
|
+
import { CotalEndpoint, canonicalJson, copyPersonaEndpoints, lowerPersonaEndpoints, parsePersonaEndpoints, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, MANAGER_LEASE_TTL_MS, awaitLeaseExpiry, leaseRemainingMs, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, assertValidChannel, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, fenceLifecycleAcl, DeprovisionError, addDeprovisionAccounting, isConcreteChannel, isChannelFamily, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, identityFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, bindIssuedStores, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, oneLine, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, runDriverCaller, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannel, eventChannelPrincipal, resolveService, invokeCommand, digestIdToken, capabilityWithin, grantOverreach, grantParts, isIssuedCaller, issuedSpawnerStanding, withIssuerSession, renderOnceTask, } from "@cotal-ai/core";
|
|
9
9
|
import { loadLaunchPersonaFile, PersonaSourceError } from "@cotal-ai/workspace";
|
|
10
10
|
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimIdentityRecord, claimManagerSiblingIdentities, connectorInstallHint, DEFAULT_SEAT_CAP, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, probeLiveness, remintDaemonCreds, resolveAgentType, resolveOnPath, runtimeInstallHint, createManagerInstanceIdentity, SEAT_CAP_UNLIMITED, spaceKey, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
11
11
|
import { createRuntime, exitProof, isCustodialRuntime, pollProvedExit, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, watchProvedExit, } from "./runtime/index.js";
|
|
@@ -264,6 +264,25 @@ function ownLists(form) {
|
|
|
264
264
|
...(form.shareTools && { shareTools: [...form.shareTools] }),
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
|
+
/** The inline record is admitted the same way, one level deeper: the record itself, its lists, and
|
|
268
|
+
* each `endpoints` entry with its commands. It is still untrusted here, so a shape the validator
|
|
269
|
+
* will refuse is copied as far as it goes and refused where it is read. */
|
|
270
|
+
function ownPersona(record) {
|
|
271
|
+
if (record === null || typeof record !== "object" || Array.isArray(record))
|
|
272
|
+
return record;
|
|
273
|
+
const owned = ownLists({ ...record });
|
|
274
|
+
const endpoints = owned.endpoints;
|
|
275
|
+
if (Array.isArray(endpoints))
|
|
276
|
+
owned.endpoints = endpoints.map((e) => {
|
|
277
|
+
if (e === null || typeof e !== "object" || Array.isArray(e))
|
|
278
|
+
return e;
|
|
279
|
+
const entry = { ...e };
|
|
280
|
+
if (Array.isArray(entry.commands))
|
|
281
|
+
entry.commands = [...entry.commands];
|
|
282
|
+
return entry;
|
|
283
|
+
});
|
|
284
|
+
return owned;
|
|
285
|
+
}
|
|
267
286
|
/** `start()`'s refusal when this manager instance's lease is held (#3915): by another live process
|
|
268
287
|
* on this workspace root, or by a holder on another root. A restart whose predecessor on this root
|
|
269
288
|
* is dead, this process's own earlier start included, waits the key out instead (#4092). The
|
|
@@ -331,7 +350,7 @@ function onceStopReason(cause) {
|
|
|
331
350
|
function revokedRunRefusal(runId, revoked) {
|
|
332
351
|
return `run ${runId} was revoked by ${revoked.by} (${revoked.reason}); a revoked run starts no helper (SPEC 14.8)`;
|
|
333
352
|
}
|
|
334
|
-
const INLINE_PERSONA_KEYS = ["prompt", "role", "model", "variant", "description", "subscribe", "allowSubscribe", "allowPublish", "capabilities", "save"];
|
|
353
|
+
const INLINE_PERSONA_KEYS = ["prompt", "role", "model", "variant", "description", "subscribe", "allowSubscribe", "allowPublish", "capabilities", "endpoints", "save"];
|
|
335
354
|
/** Read an inline persona into the definition a catalog file would load as, refusing what a file
|
|
336
355
|
* load refuses plus any shape the closed record does not take. The body is literal text: nothing in
|
|
337
356
|
* it becomes a field, so a body that starts with `---` stays body. */
|
|
@@ -374,6 +393,7 @@ function inlinePersonaDef(name, raw) {
|
|
|
374
393
|
allowSubscribe: list("allowSubscribe"),
|
|
375
394
|
allowPublish: list("allowPublish"),
|
|
376
395
|
capabilities: list("capabilities"),
|
|
396
|
+
endpoints: parsePersonaEndpoints(rec.endpoints, "persona"),
|
|
377
397
|
};
|
|
378
398
|
// The checks a file load makes on these fields: channel names, and the read set within the read ACL.
|
|
379
399
|
try {
|
|
@@ -386,6 +406,11 @@ function inlinePersonaDef(name, raw) {
|
|
|
386
406
|
}
|
|
387
407
|
return { def, save: rec.save === true };
|
|
388
408
|
}
|
|
409
|
+
/** The refusal a user-auth mesh gives a persona that names `endpoints`. */
|
|
410
|
+
function userMeshEndpointsRefusal(door) {
|
|
411
|
+
return `${door}: this mesh has per-user auth, where endpoint access comes from the service owner's grants and a persona cannot hand it on (SPEC 13.15). ` +
|
|
412
|
+
"Drop `endpoints` from the persona, and ask the operator to grant the helper with `cotal service-endpoint grant` once it runs.";
|
|
413
|
+
}
|
|
389
414
|
const TURN_PARTICIPATION_TTL_MS = 5 * 60_000;
|
|
390
415
|
function participantKey(owner, actor, uid) {
|
|
391
416
|
return `${owner}.${actor}.${uid}`;
|
|
@@ -533,6 +558,7 @@ export class Manager {
|
|
|
533
558
|
secretStoreIdentity;
|
|
534
559
|
/** See {@link ManagerOptions.installedExtensions}. */
|
|
535
560
|
installedExtensions;
|
|
561
|
+
expectedConnectorGeneration;
|
|
536
562
|
executionState;
|
|
537
563
|
/** One backend per kind a seat has asked for, beside {@link runtime}. The manager's own runtime
|
|
538
564
|
* stays the default and is never replaced by a later seat's choice. */
|
|
@@ -815,6 +841,7 @@ export class Manager {
|
|
|
815
841
|
leaseStopping = false;
|
|
816
842
|
/** The class-2 renewal owner's half-TTL schedule; armed only on auth meshes. */
|
|
817
843
|
credRenewTimer;
|
|
844
|
+
resumeCredRenewal;
|
|
818
845
|
/** #2073: the `endpoint-serve` credential's OWN renewal, armed from its `[renewAt, exp)` window
|
|
819
846
|
* (`credsRenewalDelayMs`) rather than left to `credRenewTimer`'s boot-phased TTL/4 tick, which can
|
|
820
847
|
* miss the window by ordering. Re-armed at registration and after every successful re-mint;
|
|
@@ -871,6 +898,8 @@ export class Manager {
|
|
|
871
898
|
return dialerFor(servers)({ ...opts, servers });
|
|
872
899
|
}
|
|
873
900
|
constructor(opts) {
|
|
901
|
+
if (opts.expectedConnectorGeneration !== undefined && opts.expectedConnectorGeneration !== MANAGER_VERSION)
|
|
902
|
+
throw new Error(`expected connector generation ${opts.expectedConnectorGeneration} differs from this Manager's loaded generation ${MANAGER_VERSION}; restart from the installed CLI`);
|
|
874
903
|
if (opts.onServiceIdentity !== undefined && typeof opts.onServiceIdentity !== "function")
|
|
875
904
|
throw new Error("manager onServiceIdentity must be a trusted embedding callback");
|
|
876
905
|
if (opts.execution !== undefined && opts.execution !== "runtime" && opts.execution !== "none")
|
|
@@ -906,6 +935,7 @@ export class Manager {
|
|
|
906
935
|
if (opts.secretStore)
|
|
907
936
|
this.secretStoreIdentity = injectedSecretStoreIdentity(opts.secretStore);
|
|
908
937
|
this.installedExtensions = opts.installedExtensions ?? false;
|
|
938
|
+
this.expectedConnectorGeneration = opts.expectedConnectorGeneration;
|
|
909
939
|
this.pooled = opts.pooled === true;
|
|
910
940
|
this.executionState = opts.execution === "none" ? { kind: "none" } : { kind: "runtime" };
|
|
911
941
|
if (!this.remoteAuthority) {
|
|
@@ -1467,24 +1497,29 @@ export class Manager {
|
|
|
1467
1497
|
catch { }
|
|
1468
1498
|
}
|
|
1469
1499
|
const intervalMs = Math.min(credRenewIntervalMs(5 * 60), credRenewIntervalMs(standingTtl));
|
|
1470
|
-
this.
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1500
|
+
this.resumeCredRenewal = () => {
|
|
1501
|
+
if (this.credRenewTimer || this.leaseStopping)
|
|
1502
|
+
return;
|
|
1503
|
+
this.credRenewTimer = setInterval(async () => {
|
|
1504
|
+
try {
|
|
1505
|
+
if (this.remoteAuthority?.renewStandingBundle)
|
|
1506
|
+
await this.renewRemoteStandingBundle();
|
|
1507
|
+
else
|
|
1508
|
+
await this.renewRemoteExecutor();
|
|
1509
|
+
}
|
|
1510
|
+
catch (e) {
|
|
1511
|
+
console.error(`! remote manager renewal pass failed: ${rejectionText(e)}`);
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
await this.runHosting?.renew();
|
|
1515
|
+
}
|
|
1516
|
+
catch (e) {
|
|
1517
|
+
console.error(`! remote run-hosting renewal pass failed: ${rejectionText(e)}`);
|
|
1518
|
+
}
|
|
1519
|
+
}, intervalMs);
|
|
1520
|
+
this.credRenewTimer.unref?.();
|
|
1521
|
+
};
|
|
1522
|
+
this.resumeCredRenewal();
|
|
1488
1523
|
}
|
|
1489
1524
|
// stop() fences before it waits for an accepted startup reconciliation terminal. Once that
|
|
1490
1525
|
// terminal drains, start() and stop() resume from the same await boundary; start must observe
|
|
@@ -2297,6 +2332,7 @@ export class Manager {
|
|
|
2297
2332
|
this.credRenewTimer = setInterval(() => { void this.renewDaemonCreds(); }, credRenewIntervalMs(STANDING_RENEWABLE_TTL_SEC));
|
|
2298
2333
|
this.credRenewTimer.unref?.();
|
|
2299
2334
|
}
|
|
2335
|
+
this.resumeCredRenewal?.();
|
|
2300
2336
|
// Exit watchers were suppressed while the fence stood: reconcile every child that died during
|
|
2301
2337
|
// preparation now, or its slot/credential footprint would linger unreaped after the abort.
|
|
2302
2338
|
for (const agent of [...this.agents.values()]) {
|
|
@@ -2484,6 +2520,7 @@ export class Manager {
|
|
|
2484
2520
|
allowSubscribe: a.launch.allowSubscribe,
|
|
2485
2521
|
allowPublish: a.launch.allowPublish,
|
|
2486
2522
|
capabilities: a.launch.capabilities,
|
|
2523
|
+
...(a.launch.endpoints?.length ? { endpoints: copyPersonaEndpoints(a.launch.endpoints) } : {}),
|
|
2487
2524
|
events: a.launch.events,
|
|
2488
2525
|
shareTools: a.launch.shareTools,
|
|
2489
2526
|
forkSource: a.launch.forkSource,
|
|
@@ -5569,6 +5606,7 @@ export class Manager {
|
|
|
5569
5606
|
* on MANIFEST membership FIRST — so a live `cotal ext remove` is honored even though the registry
|
|
5570
5607
|
* still holds a connector imported earlier this session — then returns the registry hit or lazily
|
|
5571
5608
|
* imports the providing package (transactional + single-flight, via the workspace primitive).
|
|
5609
|
+
* An expected generation also revalidates cached first-party providers under that lock.
|
|
5572
5610
|
* Fail-loud with an install hint; no fallback. */
|
|
5573
5611
|
async resolveConnector(name) {
|
|
5574
5612
|
if (!this.installedExtensions)
|
|
@@ -5576,7 +5614,9 @@ export class Manager {
|
|
|
5576
5614
|
if (!manifestExtensionNames("connector").includes(name))
|
|
5577
5615
|
throw new Error(connectorInstallHint(name));
|
|
5578
5616
|
const already = registry.all("connector").find((c) => c.name === name);
|
|
5579
|
-
|
|
5617
|
+
if (this.expectedConnectorGeneration === undefined && already)
|
|
5618
|
+
return already;
|
|
5619
|
+
return materializeFromManifest({ kind: "connector", name }, { hint: (ref) => connectorInstallHint(ref.name), expectedGeneration: this.expectedConnectorGeneration });
|
|
5580
5620
|
}
|
|
5581
5621
|
/** Connector names the manager can spawn WITHOUT importing: the manifest on the published binary,
|
|
5582
5622
|
* else whatever a composition root registered. Drives the `models` catalog enumeration. */
|
|
@@ -6179,6 +6219,8 @@ export class Manager {
|
|
|
6179
6219
|
opts = ownLists(opts);
|
|
6180
6220
|
if (opts.resolved)
|
|
6181
6221
|
opts.resolved = ownLists(opts.resolved);
|
|
6222
|
+
if (opts.persona !== undefined)
|
|
6223
|
+
opts.persona = ownPersona(opts.persona);
|
|
6182
6224
|
if (opts.supervise) {
|
|
6183
6225
|
opts.supervise = { restarts: opts.supervise.restarts, windowMs: opts.supervise.windowMs };
|
|
6184
6226
|
// The budget is checked here, where it is held, not only at the wire: an in-process caller of
|
|
@@ -6355,6 +6397,10 @@ export class Manager {
|
|
|
6355
6397
|
let allowSubscribe;
|
|
6356
6398
|
let allowPublish;
|
|
6357
6399
|
let capabilities;
|
|
6400
|
+
// The endpoint commands the persona hands on (SPEC 13.15), as the persona names them and as the
|
|
6401
|
+
// mint grants them. A manifest launch carries none: its schema has no slot for them.
|
|
6402
|
+
let personaEndpoints;
|
|
6403
|
+
let endpoints = [];
|
|
6358
6404
|
let model = opts.model;
|
|
6359
6405
|
let variant = opts.variant;
|
|
6360
6406
|
let launchOptions = opts.launchOptions;
|
|
@@ -6419,6 +6465,13 @@ export class Manager {
|
|
|
6419
6465
|
allowSubscribe = opts.allowSubscribe ?? def.allowSubscribe ?? scoped?.allowSubscribe ?? [];
|
|
6420
6466
|
allowPublish = opts.allowPublish ?? def.allowPublish ?? scoped?.allowPublish;
|
|
6421
6467
|
capabilities = def.capabilities;
|
|
6468
|
+
personaEndpoints = copyPersonaEndpoints(def.endpoints);
|
|
6469
|
+
// A user-auth mesh grants endpoint access through the service owner's records, which a spawn
|
|
6470
|
+
// cannot write (SPEC 13.15), so a persona that names the field is refused there, `[]` included,
|
|
6471
|
+
// before any bound or exemption reads it.
|
|
6472
|
+
if (personaEndpoints !== undefined && this.meshMode === "user")
|
|
6473
|
+
return { ok: false, error: userMeshEndpointsRefusal("spawn") };
|
|
6474
|
+
endpoints = lowerPersonaEndpoints(personaEndpoints);
|
|
6422
6475
|
// #651: fold the persona's model into the launch record, mirroring the variant line below
|
|
6423
6476
|
// and the manifest branch above. Without this, a persona-file model (the common pin source)
|
|
6424
6477
|
// never reaches `launch.model`, so the connector runs the seat on it while `ps --wide`/`--json`
|
|
@@ -6447,6 +6500,7 @@ export class Manager {
|
|
|
6447
6500
|
...(allowPublish !== undefined ? { allowPublish } : {}),
|
|
6448
6501
|
...(role !== undefined ? { role } : {}),
|
|
6449
6502
|
...(capabilities !== undefined ? { capabilities } : {}),
|
|
6503
|
+
...(endpoints.length ? { endpoints } : {}),
|
|
6450
6504
|
}, events);
|
|
6451
6505
|
if (boundRefusal)
|
|
6452
6506
|
return { ok: false, error: boundRefusal };
|
|
@@ -6468,6 +6522,7 @@ export class Manager {
|
|
|
6468
6522
|
allowSubscribe: [...allowSubscribe],
|
|
6469
6523
|
allowPublish: [...(allowPublish ?? [])],
|
|
6470
6524
|
capabilities: capabilities?.length ? [...capabilities] : undefined,
|
|
6525
|
+
endpoints: copyPersonaEndpoints(personaEndpoints?.length ? personaEndpoints : undefined),
|
|
6471
6526
|
}
|
|
6472
6527
|
: undefined;
|
|
6473
6528
|
// Checked after every path has resolved its selectors: a direct `startAgent` call skips the
|
|
@@ -6827,6 +6882,7 @@ export class Manager {
|
|
|
6827
6882
|
allowPublish,
|
|
6828
6883
|
role,
|
|
6829
6884
|
capabilities,
|
|
6885
|
+
...(endpoints.length ? { endpointCapabilities: endpoints } : {}),
|
|
6830
6886
|
lifecycleUid,
|
|
6831
6887
|
expiresAt: exp,
|
|
6832
6888
|
issued,
|
|
@@ -6902,6 +6958,9 @@ export class Manager {
|
|
|
6902
6958
|
allowSubscribe,
|
|
6903
6959
|
allowPublish,
|
|
6904
6960
|
capabilities,
|
|
6961
|
+
// Always the list the launch resolved, `[]` included, so the session never reads a persona
|
|
6962
|
+
// file that may have changed since.
|
|
6963
|
+
endpoints: copyPersonaEndpoints(personaEndpoints) ?? [],
|
|
6905
6964
|
events,
|
|
6906
6965
|
cwd,
|
|
6907
6966
|
};
|
|
@@ -6977,6 +7036,7 @@ export class Manager {
|
|
|
6977
7036
|
allowSubscribe,
|
|
6978
7037
|
allowPublish,
|
|
6979
7038
|
capabilities,
|
|
7039
|
+
...(personaEndpoints?.length ? { endpoints: copyPersonaEndpoints(personaEndpoints) } : {}),
|
|
6980
7040
|
events,
|
|
6981
7041
|
shareTools: opts.shareTools,
|
|
6982
7042
|
forkSource: opts.resume,
|
|
@@ -7707,6 +7767,7 @@ export class Manager {
|
|
|
7707
7767
|
allowSubscribe: entry.launch.allowSubscribe,
|
|
7708
7768
|
allowPublish: entry.launch.allowPublish,
|
|
7709
7769
|
capabilities: entry.launch.capabilities,
|
|
7770
|
+
endpoints: copyPersonaEndpoints(entry.launch.endpoints) ?? [],
|
|
7710
7771
|
events: entry.launch.events,
|
|
7711
7772
|
cwd: entry.launch.cwd,
|
|
7712
7773
|
};
|
|
@@ -7791,6 +7852,7 @@ export class Manager {
|
|
|
7791
7852
|
allowSubscribe: entry.launch.allowSubscribe,
|
|
7792
7853
|
allowPublish: entry.launch.allowPublish,
|
|
7793
7854
|
capabilities: entry.launch.capabilities,
|
|
7855
|
+
...(entry.launch.endpoints?.length ? { endpoints: copyPersonaEndpoints(entry.launch.endpoints) } : {}),
|
|
7794
7856
|
events: entry.launch.events,
|
|
7795
7857
|
shareTools: entry.launch.shareTools,
|
|
7796
7858
|
forkSource: entry.launch.forkSource,
|
|
@@ -8477,6 +8539,8 @@ export class Manager {
|
|
|
8477
8539
|
await nc.drain().catch(() => nc.close());
|
|
8478
8540
|
}
|
|
8479
8541
|
}
|
|
8542
|
+
/** Parent-local inventory read. It does not enter maintenance or change admission. */
|
|
8543
|
+
hostedSeatCount() { return this.agents.size; }
|
|
8480
8544
|
/** The served manager-level health summary. Given an owner, `agentCount` counts only that owner's
|
|
8481
8545
|
* seats, as `ps` lists them ({@link psOwnerFilter}), so a person reading a shared Manager does not
|
|
8482
8546
|
* learn how many seats other owners hold on it. */
|
|
@@ -11053,6 +11117,7 @@ export class Manager {
|
|
|
11053
11117
|
allowSubscribe: entry.launch.allowSubscribe,
|
|
11054
11118
|
allowPublish: entry.launch.allowPublish,
|
|
11055
11119
|
capabilities: entry.launch.capabilities,
|
|
11120
|
+
...(entry.launch.endpoints?.length ? { endpoints: copyPersonaEndpoints(entry.launch.endpoints) } : {}),
|
|
11056
11121
|
events: entry.launch.events,
|
|
11057
11122
|
shareTools: entry.launch.shareTools,
|
|
11058
11123
|
forkSource: entry.launch.forkSource,
|
|
@@ -11505,6 +11570,7 @@ export class Manager {
|
|
|
11505
11570
|
allowPublish: a.launch.allowPublish,
|
|
11506
11571
|
role: a.role,
|
|
11507
11572
|
capabilities: a.launch.capabilities,
|
|
11573
|
+
...(a.launch.endpoints?.length ? { endpointCapabilities: lowerPersonaEndpoints(a.launch.endpoints) } : {}),
|
|
11508
11574
|
lifecycleUid: a.lifecycleUid,
|
|
11509
11575
|
expiresAt: exp,
|
|
11510
11576
|
issued,
|
|
@@ -12081,6 +12147,13 @@ export class Manager {
|
|
|
12081
12147
|
return (Array.isArray(v) ? v : [v]).map((x) => String(x));
|
|
12082
12148
|
};
|
|
12083
12149
|
const capabilities = strList(args.capabilities);
|
|
12150
|
+
let endpointArg;
|
|
12151
|
+
try {
|
|
12152
|
+
endpointArg = parsePersonaEndpoints(args.endpoints, "define-persona");
|
|
12153
|
+
}
|
|
12154
|
+
catch (e) {
|
|
12155
|
+
return { ok: false, error: rejectionText(e) };
|
|
12156
|
+
}
|
|
12084
12157
|
// Read, authorize and compose against the catalog as it is now. Nothing yields between this and
|
|
12085
12158
|
// the write, so no other call in this manager can change the file in between. The handler does
|
|
12086
12159
|
// not serialize with other processes; their writes are outside this single-manager guarantee.
|
|
@@ -12111,6 +12184,7 @@ export class Manager {
|
|
|
12111
12184
|
allowSubscribe: strList(args.allowSubscribe),
|
|
12112
12185
|
allowPublish: strList(args.allowPublish),
|
|
12113
12186
|
capabilities,
|
|
12187
|
+
endpoints: copyPersonaEndpoints(endpointArg),
|
|
12114
12188
|
}, existing),
|
|
12115
12189
|
};
|
|
12116
12190
|
}
|
|
@@ -12121,23 +12195,34 @@ export class Manager {
|
|
|
12121
12195
|
let composed = compose();
|
|
12122
12196
|
if ("reply" in composed)
|
|
12123
12197
|
return composed.reply;
|
|
12124
|
-
// The bound reads the persona as written, so a redefine that omits `capabilities`
|
|
12125
|
-
// keeps checked too. Resolving the definer's grant yields, so the catalog is read
|
|
12126
|
-
|
|
12127
|
-
|
|
12198
|
+
// The bound reads the persona as written, so a redefine that omits `capabilities` or `endpoints`
|
|
12199
|
+
// has the list it keeps checked too. Resolving the definer's grant yields, so the catalog is read
|
|
12200
|
+
// again after it.
|
|
12201
|
+
if (composed.def.capabilities?.length || composed.def.endpoints !== undefined) {
|
|
12202
|
+
const refusal = await this.definerGrantRefusal(caller, authenticated);
|
|
12128
12203
|
composed = compose();
|
|
12129
12204
|
if ("reply" in composed)
|
|
12130
12205
|
return composed.reply;
|
|
12131
12206
|
const kept = composed.def.capabilities ?? [];
|
|
12132
|
-
const
|
|
12133
|
-
|
|
12207
|
+
const keptEndpoints = lowerPersonaEndpoints(composed.def.endpoints);
|
|
12208
|
+
// A user-auth persona may not name the field at all; elsewhere `[]` hands on nothing to bound.
|
|
12209
|
+
const namesEndpoints = composed.def.endpoints !== undefined;
|
|
12210
|
+
const refused = kept.length || keptEndpoints.length || (namesEndpoints && this.meshMode === "user")
|
|
12211
|
+
? refusal({ capabilities: kept, endpoints: keptEndpoints, namesEndpoints })
|
|
12212
|
+
: undefined;
|
|
12213
|
+
if (refused) {
|
|
12214
|
+
const fromSaved = [
|
|
12215
|
+
...(capabilities === undefined && kept.length ? ["capabilities"] : []),
|
|
12216
|
+
...(endpointArg === undefined && composed.def.endpoints?.length ? ["endpoints"] : []),
|
|
12217
|
+
];
|
|
12134
12218
|
return {
|
|
12135
12219
|
ok: false,
|
|
12136
12220
|
code: "permission-denied",
|
|
12137
|
-
error:
|
|
12138
|
-
? `${refused}
|
|
12221
|
+
error: fromSaved.length
|
|
12222
|
+
? `${refused} ${fromSaved.map((f) => `\`${f}\``).join(" and ")} come${fromSaved.length === 1 ? "s" : ""} from the saved persona, which a redefine that omits ${fromSaved.length === 1 ? "it keeps" : "them keeps"}; pass what the definer holds, or [] to clear ${fromSaved.length === 1 ? "it" : "them"}.`
|
|
12139
12223
|
: refused,
|
|
12140
12224
|
};
|
|
12225
|
+
}
|
|
12141
12226
|
}
|
|
12142
12227
|
try {
|
|
12143
12228
|
saveAgentFile(path, composed.def);
|
|
@@ -12148,24 +12233,29 @@ export class Manager {
|
|
|
12148
12233
|
return { ok: true, data: { name, path } };
|
|
12149
12234
|
}
|
|
12150
12235
|
/** Resolve what a persona's definer may put into it, as a check that returns the refusal for
|
|
12151
|
-
* capabilities it does not hold, or `undefined` when it holds them all. The
|
|
12152
|
-
* yield. An open mesh mints no credential, so nothing is bounded there. A
|
|
12153
|
-
* bounded by its issued evidence, as a spawner is; a user-auth definer by its
|
|
12154
|
-
* scope, under the ledger's token rule, with `admin` exempt. A
|
|
12236
|
+
* capabilities or endpoint commands it does not hold, or `undefined` when it holds them all. The
|
|
12237
|
+
* check itself does not yield. An open mesh mints no credential, so nothing is bounded there. A
|
|
12238
|
+
* static definer is bounded by its issued evidence, as a spawner is; a user-auth definer by its
|
|
12239
|
+
* own ledger row's scope, under the ledger's token rule, with `admin` exempt. A user-auth persona
|
|
12240
|
+
* cannot name `endpoints` at all, `[]` included (SPEC 13.15). A hosted manager reads no ledger, so
|
|
12155
12241
|
* it refuses. A later spawn of the persona is bounded again against its own spawner. */
|
|
12156
|
-
async
|
|
12242
|
+
async definerGrantRefusal(caller, authenticated) {
|
|
12157
12243
|
if (this.meshMode === "open")
|
|
12158
12244
|
return () => undefined;
|
|
12159
12245
|
if (this.meshMode === "static") {
|
|
12160
12246
|
const run = await this.spawnRun(authenticated, undefined);
|
|
12161
12247
|
const bound = await this.staticSpawnBound(authenticated, run !== undefined && "admission" in run ? run : undefined);
|
|
12162
|
-
return (capabilities) => this.spawnBoundRefusal(bound, { capabilities }, false, "define-persona");
|
|
12248
|
+
return ({ capabilities, endpoints }) => this.spawnBoundRefusal(bound, { capabilities, ...(endpoints.length ? { endpoints } : {}) }, false, "define-persona");
|
|
12163
12249
|
}
|
|
12164
12250
|
if (this.remoteAuthority)
|
|
12165
|
-
return (
|
|
12251
|
+
return ({ capabilities, namesEndpoints }) => namesEndpoints
|
|
12252
|
+
? userMeshEndpointsRefusal("define-persona")
|
|
12253
|
+
: capabilities.length ? "define-persona: this manager's user authority is hosted, so it cannot read the definer's grant and its persona can grant no capabilities." : undefined;
|
|
12166
12254
|
const p = parsePrincipalKey(caller);
|
|
12167
12255
|
const scope = p ? await resolveAuthProvider().actorScope({ dir: userAuthStateDir(this.workspaceRoot, this.space), owner: p.owner, actor: p.actor }) : undefined;
|
|
12168
|
-
return (capabilities) => {
|
|
12256
|
+
return ({ capabilities, namesEndpoints }) => {
|
|
12257
|
+
if (namesEndpoints)
|
|
12258
|
+
return userMeshEndpointsRefusal("define-persona");
|
|
12169
12259
|
if (scope?.includes("admin"))
|
|
12170
12260
|
return undefined;
|
|
12171
12261
|
const over = capabilities.filter((c) => !capabilityWithin(scope ?? [], c));
|