@cotal-ai/manager 0.20.1 → 0.22.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/authorize.d.ts +1 -1
- package/dist/authorize.js +2 -2
- package/dist/authorize.js.map +1 -1
- package/dist/manager-service-contract.d.ts +11 -3
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +47 -5
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +57 -10
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +208 -21
- package/dist/manager.js.map +1 -1
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +5 -1
- package/dist/resume.js.map +1 -1
- package/dist/runtime/pty.d.ts.map +1 -1
- package/dist/runtime/pty.js +8 -0
- package/dist/runtime/pty.js.map +1 -1
- package/package.json +4 -4
package/dist/manager.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash, randomUUID, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { connect, credsAuthenticator } from "@nats-io/transport-node";
|
|
4
4
|
import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
|
|
5
5
|
import { join, dirname, resolve } from "node:path";
|
|
6
|
-
import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_TTL_MS, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, connectorServers, deprovisionAgent, firstFreeName, idFromCreds, inspectCredHealth, loadAgentFile, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, } from "@cotal-ai/core";
|
|
6
|
+
import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_TTL_MS, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, connectorServers, deprovisionAgent, firstFreeName, idFromCreds, inspectCredHealth, loadAgentFile, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
|
|
7
7
|
import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, connectorInstallHint, DEFAULT_CONNECTOR, defaultAgentType, DELIVERY_CREDS_KEY, findCotalRoot, getSpaceAuth, hasUserAuthState, loadManagerInstanceIdentity, loadMeshes, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KEY, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, saveManagerInstanceIdentity, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
|
|
8
8
|
import { createRuntime, } from "./runtime/index.js";
|
|
9
9
|
import { AttachEndpoint } from "./attach-endpoint.js";
|
|
@@ -146,6 +146,32 @@ export async function epAwaitReply(nc, space, caller, nonce, requestId, requestS
|
|
|
146
146
|
* through a pluggable {@link Runtime} (pty by default). It does NOT proxy agent
|
|
147
147
|
* mesh traffic — terminal I/O streams over its own attach endpoint instead.
|
|
148
148
|
*/
|
|
149
|
+
/**
|
|
150
|
+
* Event channels in `channels` that do NOT belong to `{owner, actor}`.
|
|
151
|
+
*
|
|
152
|
+
* ONE HELPER FOR EVERY SEAM THAT ARMS AN ACL, and that is the whole design. The rule first existed
|
|
153
|
+
* only at the spawn accept seam, and a security review found the hole that shape guarantees: resume
|
|
154
|
+
* re-arms a managed row straight from the inventory document, and `renewManagedStaticCred` re-mints
|
|
155
|
+
* the JWT from that row, so an admin-supplied inventory could carry a foreign concrete event channel
|
|
156
|
+
* past a fence that only ever looked at spawns. A rule with one call site is a rule that covers one
|
|
157
|
+
* door.
|
|
158
|
+
*
|
|
159
|
+
* IT COMPARES PRINCIPALS, NOT STRINGS. `eventChannelPrincipal` decodes the channel back to the
|
|
160
|
+
* `{owner, actor}` it names, so this is mode-independent: static keys the actor on the allocated
|
|
161
|
+
* nkey and user mode keys it on the alias, and neither needs its own branch here.
|
|
162
|
+
*
|
|
163
|
+
* STATED LIMIT, unchanged and asserted by a cell: the decode refuses anything that is not exactly
|
|
164
|
+
* two principal tokens, so a WILDCARD is not an event channel to it. `events.<owner>.>` and
|
|
165
|
+
* `events.>` pass untouched and are governed by ordinary ACL authority. This closes the concrete
|
|
166
|
+
* form, which is what a caller writes when it knows which agent it wants to read, and not the
|
|
167
|
+
* wildcard form, which is what an operator writes deliberately for an observer.
|
|
168
|
+
*/
|
|
169
|
+
function foreignEventChannels(channels, owner, actor) {
|
|
170
|
+
return channels.filter((ch) => {
|
|
171
|
+
const p = eventChannelPrincipal(ch);
|
|
172
|
+
return p !== null && !(p.owner === owner && p.actor === actor);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
149
175
|
export class Manager {
|
|
150
176
|
space;
|
|
151
177
|
servers;
|
|
@@ -366,6 +392,44 @@ export class Manager {
|
|
|
366
392
|
get consoleUrl() {
|
|
367
393
|
return this.attach.consoleUrl();
|
|
368
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* The out-of-band route for the mesh the refusal is running on, as ONE paste-ready command.
|
|
397
|
+
*
|
|
398
|
+
* **A REMEDY A REFUSAL PRINTS IS AUTHORITY THE REFUSAL LENDS, AND BOTH HALVES OF THIS ONE WERE
|
|
399
|
+
* WIDER THAN THE SENTENCE AROUND THEM.** The static half named `--profile observer`, and `mint`
|
|
400
|
+
* reads `--allow-subscribe` only for the agent profile while the observer arm of `permissionsFor`
|
|
401
|
+
* hardcodes `chat.>`: an operator narrowing a reader to one event plane was handed a reader of every
|
|
402
|
+
* channel in the space. The user half named a bare `cotal actor grant`, and an omitted flag there is
|
|
403
|
+
* not "leave it alone" but the WIDE default (`>` read, `>` post, `spawn,role:default` scope), so the same
|
|
404
|
+
* sentence handed out a full-mesh reader-writer with spawn. The static half was found by RUNNING
|
|
405
|
+
* the printed command and decoding what it produced; the user half was found by READING it against
|
|
406
|
+
* `runActor`'s defaults. Neither is the only way in, and a remedy string is not proved by either
|
|
407
|
+
* one alone.
|
|
408
|
+
*
|
|
409
|
+
* So the command is spelled out in full and only ONE is printed: the one for the mesh this manager
|
|
410
|
+
* is actually running, because a sentence carrying both routes is a sentence an operator picks the
|
|
411
|
+
* wrong half of. `smoke:events-grant` section 9 runs the static half and grades the credential;
|
|
412
|
+
* `smoke:user-spawn:live` section E runs the user half and grades the row.
|
|
413
|
+
*
|
|
414
|
+
* It takes NO mode argument, on purpose. It used to, and the resume door passed the resumed
|
|
415
|
+
* DOCUMENT's (`entry.identity.mode === "user"`) rather than the manager's: those agree on an honest
|
|
416
|
+
* inventory and disagree on the shape section 8 exercises, a user-mode record under a static
|
|
417
|
+
* manager, where it handed a static operator `cotal actor grant` for a mesh with no actor ledger to
|
|
418
|
+
* write it to. The operator reading the refusal is on the manager's mesh, never on the record's. A
|
|
419
|
+
* boolean parameter is how that happened, so the mode is read from `this` and a third door cannot
|
|
420
|
+
* pass the wrong one.
|
|
421
|
+
*/
|
|
422
|
+
readerRemedy(owner, channel) {
|
|
423
|
+
return this.userMode
|
|
424
|
+
? `\`cotal actor grant <reader> --owner ${owner} --scope '' --allow-subscribe '${channel}' ` +
|
|
425
|
+
`--allow-publish ''\`, with every field spelled out: \`actor grant\` is an upsert of the ` +
|
|
426
|
+
`WHOLE row and an omitted flag means the WIDE default (\`>\` read, \`>\` post, \`spawn,role:default\` ` +
|
|
427
|
+
`scope), not "leave it alone".`
|
|
428
|
+
: `\`cotal mint <reader> --profile agent --allow-subscribe ${channel} --provision\`, where there ` +
|
|
429
|
+
`is no actor ledger for \`actor grant\` to write to. The AGENT profile, not the observer ` +
|
|
430
|
+
`one: \`mint\` reads --allow-subscribe only for that profile, so an observer mint is ` +
|
|
431
|
+
`refused outright and writes no creds file.`;
|
|
432
|
+
}
|
|
369
433
|
async start() {
|
|
370
434
|
await this.attach.start();
|
|
371
435
|
// In auth mode the manager is just another user in the space's account — it mints
|
|
@@ -1051,7 +1115,7 @@ export class Manager {
|
|
|
1051
1115
|
allowSubscribe: a.launch.allowSubscribe,
|
|
1052
1116
|
allowPublish: a.launch.allowPublish,
|
|
1053
1117
|
capabilities: a.launch.capabilities,
|
|
1054
|
-
|
|
1118
|
+
events: a.launch.events,
|
|
1055
1119
|
shareTools: a.launch.shareTools,
|
|
1056
1120
|
forkSource: a.launch.forkSource,
|
|
1057
1121
|
unresolvedLaunchOptionKeys: a.launch.unresolvedLaunchOptionKeys,
|
|
@@ -1625,6 +1689,16 @@ export class Manager {
|
|
|
1625
1689
|
throw new EpEnvelopeError("permission-denied", denied);
|
|
1626
1690
|
return unwrap(await this.attachAuthorized(a, ctx.subject.caller));
|
|
1627
1691
|
}),
|
|
1692
|
+
// C3 `input`: the SAME authorization as `attach`, deliberately written out rather than
|
|
1693
|
+
// factored with it - the two share a policy, not a body, and a shared wrapper would be a
|
|
1694
|
+
// place for one of them to quietly acquire a condition the other does not have.
|
|
1695
|
+
input: (ctx) => this.serveGated(ctx, async () => {
|
|
1696
|
+
const a = targetAgent(ctx);
|
|
1697
|
+
const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx));
|
|
1698
|
+
if (denied)
|
|
1699
|
+
throw new EpEnvelopeError("permission-denied", denied);
|
|
1700
|
+
return this.inputAuthorized(a, args(ctx));
|
|
1701
|
+
}),
|
|
1628
1702
|
stopSelf: (ctx) => this.serveGated(ctx, () => unwrap(this.opStopSelf(callerOf(ctx), args(ctx)))),
|
|
1629
1703
|
definePersona: (ctx) => this.serveGated(ctx, () => unwrap(this.opDefinePersona(args(ctx), callerOf(ctx), false))),
|
|
1630
1704
|
purge: (ctx) => this.serveGated(ctx, () => adminGated(ctx, async () => unwrap(await this.opPurge(args(ctx), callerOf(ctx))))),
|
|
@@ -2362,7 +2436,7 @@ export class Manager {
|
|
|
2362
2436
|
variant: args.variant ? String(args.variant) : undefined,
|
|
2363
2437
|
launchOptions: args.launchOptions,
|
|
2364
2438
|
resume: args.resume ? String(args.resume) : undefined,
|
|
2365
|
-
|
|
2439
|
+
events: typeof args.events === "boolean" ? args.events : undefined,
|
|
2366
2440
|
cwd: args.cwd ? String(args.cwd) : undefined,
|
|
2367
2441
|
prompt: args.prompt ? String(args.prompt) : undefined,
|
|
2368
2442
|
subscribe,
|
|
@@ -2722,20 +2796,19 @@ export class Manager {
|
|
|
2722
2796
|
name = this.uniqueName(identityName);
|
|
2723
2797
|
}
|
|
2724
2798
|
this.reserved.add(name);
|
|
2725
|
-
//
|
|
2726
|
-
//
|
|
2727
|
-
//
|
|
2728
|
-
//
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2731
|
-
//
|
|
2732
|
-
const
|
|
2733
|
-
if (
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
}
|
|
2738
|
-
allowPublish = [...(allowPublish ?? []), connector.transcriptChannel(name)];
|
|
2799
|
+
// The AG-UI event plane (opt-in: `--events` / COTAL_EVENTS_DEFAULT=1). Refused HERE, before
|
|
2800
|
+
// anything is minted: a connector that cannot
|
|
2801
|
+
// emit must fail before provisioning rather than after, exactly as an unsupported `resume` does.
|
|
2802
|
+
// The GRANT itself cannot be derived yet. It is keyed on the agent's PRINCIPAL, and in user mode
|
|
2803
|
+
// the principal's owner is resolved further down, so deriving it from anything in scope here
|
|
2804
|
+
// would mean guessing at the identity the child will actually connect as. It is added at the
|
|
2805
|
+
// accept seam below, where the allocated triple exists.
|
|
2806
|
+
const events = opts.events ?? process.env.COTAL_EVENTS_DEFAULT === "1";
|
|
2807
|
+
if (events && !connector.eventChannel) {
|
|
2808
|
+
// Release the just-reserved name on this fail-fast path. A leaked reserve is silent: it costs
|
|
2809
|
+
// the next spawn of this persona its un-suffixed name and nothing reports why.
|
|
2810
|
+
this.reserved.delete(name);
|
|
2811
|
+
return { ok: false, error: `connector "${connector.name}" does not publish an AG-UI event plane, but events was requested` };
|
|
2739
2812
|
}
|
|
2740
2813
|
// F2 (Unit B): a STATIC managed spawn REFUSES endpoint capabilities, fail-closed IN CODE (not
|
|
2741
2814
|
// a doc note): the static terminal has no obligation-drain/frontier steps yet, so an accepted-
|
|
@@ -2778,6 +2851,50 @@ export class Manager {
|
|
|
2778
2851
|
const agentTriple = this.userMode
|
|
2779
2852
|
? { owner: opts.owner ?? (spawner && parsePrincipalKey(spawner)?.owner.startsWith("u_") ? parsePrincipalKey(spawner).owner : DEV_OWNER), actor: name, uid: lifecycleUid }
|
|
2780
2853
|
: { owner: DEV_OWNER, actor: identity.id, uid: lifecycleUid };
|
|
2854
|
+
// THE EVENT GRANT, derived from the principal that was actually ALLOCATED and never from the
|
|
2855
|
+
// display name. A display name is UI convenience: this mesh permits two live agents to carry
|
|
2856
|
+
// one, so a name-keyed channel fuses two principals onto one subject and, in auth mode,
|
|
2857
|
+
// authorizes both onto it from the same name-only value. `agentTriple` is the triple the child
|
|
2858
|
+
// will connect as in both modes, so the grant and the subject the session derives from its own
|
|
2859
|
+
// endpoint are the same derivation. Placed here rather than beside the refusal above
|
|
2860
|
+
// because this is the first point at which that triple exists, and still before every
|
|
2861
|
+
// provisioning call that consumes `allowPublish`.
|
|
2862
|
+
// THE OWN-CHANNEL RULE FOR THE EVENT PLANE, and it is the first thing this seam does.
|
|
2863
|
+
//
|
|
2864
|
+
// An event channel carries a session's tool inputs and outputs, which makes it the most
|
|
2865
|
+
// valuable read on the mesh, and `subscribe` / `allowSubscribe` / `allowPublish` are
|
|
2866
|
+
// caller-supplied on every spawn door. On a per-user-auth mesh the ledger's spawner envelope
|
|
2867
|
+
// already refuses a delegation wider than the spawner's own grant. On a STATIC mesh there is
|
|
2868
|
+
// no ledger, so nothing attenuates a caller-supplied ACL at all, and a caller that may spawn
|
|
2869
|
+
// may mint its child a read on any subject.
|
|
2870
|
+
//
|
|
2871
|
+
// What this rule asks is NOT who the caller is. That question has no answer on a static mesh:
|
|
2872
|
+
// an untargeted spawn carries no authorization mode, and the admin reach a static caller
|
|
2873
|
+
// holds is true by construction for everyone who can reach the handler. It asks whether the
|
|
2874
|
+
// event channel being minted BELONGS TO THE AGENT BEING CREATED, which the manager knows
|
|
2875
|
+
// because it has just allocated the principal.
|
|
2876
|
+
//
|
|
2877
|
+
// STATED LIMIT, because a fence whose gap is discovered later is worse than one whose gap is
|
|
2878
|
+
// written down. `eventChannelPrincipal` decodes a principal and refuses anything that is not exactly
|
|
2879
|
+
// two principal tokens, so a WILDCARD is not an event channel to it: `events.<owner>.>` and
|
|
2880
|
+
// `events.>` pass this rule untouched and are governed by ordinary ACL authority, which on a
|
|
2881
|
+
// user mesh is the envelope and on a static mesh is the spawn credential itself. So this
|
|
2882
|
+
// closes the concrete form and not the wildcard form. It is worth having anyway: it is the
|
|
2883
|
+
// form a caller writes when it knows which agent it wants to read, and the wildcard form is
|
|
2884
|
+
// the one an operator writes deliberately for an observer.
|
|
2885
|
+
const foreign = foreignEventChannels([...allowSubscribe, ...(allowPublish ?? [])], agentTriple.owner, agentTriple.actor);
|
|
2886
|
+
if (foreign.length) {
|
|
2887
|
+
// Throws rather than returning, because this seam is inside the accept body: the throw
|
|
2888
|
+
// unwinds before `onAccepted`, so no goal is bound and no identity is minted, and the
|
|
2889
|
+
// enclosing `finally` releases the reserved name. Returning here would be a value nobody
|
|
2890
|
+
// reads.
|
|
2891
|
+
throw new Error(`this spawn asks for another agent's event channel: ${foreign.join(", ")}. An ` +
|
|
2892
|
+
`agent may be granted its OWN event plane and no other, because that plane carries the ` +
|
|
2893
|
+
`session's tool inputs and outputs. Grant a reader out of band rather than through a ` +
|
|
2894
|
+
`spawn: ${this.readerRemedy(agentTriple.owner, foreign[0])}`);
|
|
2895
|
+
}
|
|
2896
|
+
if (events)
|
|
2897
|
+
allowPublish = [...(allowPublish ?? []), connector.eventChannel({ owner: agentTriple.owner, actor: agentTriple.actor })];
|
|
2781
2898
|
await hooks?.onAccepted?.({ name, identity, lifecycleUid, agentTriple });
|
|
2782
2899
|
// In auth mode, mint the agent's creds from the space signing key and write them where the
|
|
2783
2900
|
// spawned session reads them (COTAL_CREDS path). Open mesh → no creds. Scope = the resolved
|
|
@@ -2895,7 +3012,7 @@ export class Manager {
|
|
|
2895
3012
|
allowSubscribe,
|
|
2896
3013
|
allowPublish,
|
|
2897
3014
|
capabilities,
|
|
2898
|
-
|
|
3015
|
+
events,
|
|
2899
3016
|
mcpServers,
|
|
2900
3017
|
// So a connector that keeps per-agent local state can root it at the workspace, not the
|
|
2901
3018
|
// (possibly per-agent) launch cwd below. The cwd itself rides runtime.spawn, not the launch.
|
|
@@ -2937,7 +3054,7 @@ export class Manager {
|
|
|
2937
3054
|
allowSubscribe,
|
|
2938
3055
|
allowPublish,
|
|
2939
3056
|
capabilities,
|
|
2940
|
-
|
|
3057
|
+
events,
|
|
2941
3058
|
shareTools: opts.shareTools,
|
|
2942
3059
|
forkSource: opts.resume,
|
|
2943
3060
|
// Opaque values may contain secrets. Preserve only their keys and require the referenced
|
|
@@ -3100,6 +3217,38 @@ export class Manager {
|
|
|
3100
3217
|
const referenceError = this.inventoryReferenceError(entry);
|
|
3101
3218
|
if (referenceError)
|
|
3102
3219
|
throw new Error(`retained agent ${entry.name}: ${referenceError}`);
|
|
3220
|
+
// THE OWN-CHANNEL RULE, ON THE RESUME DOOR, and it belongs here rather than beside the launch
|
|
3221
|
+
// because BOTH resume paths funnel through this function and neither may skip it.
|
|
3222
|
+
//
|
|
3223
|
+
// A resume document is admin-supplied JSON. It carries the ACLs the managed row is re-armed
|
|
3224
|
+
// from, and `renewManagedStaticCred` re-mints the credential out of that row at half TTL, so a
|
|
3225
|
+
// foreign event channel written into an inventory becomes a minted read on another agent's tool
|
|
3226
|
+
// inputs and outputs one renewal later. In static mode nothing else stops it: the checks below
|
|
3227
|
+
// pin the credential's PATH, its IDENTITY and the broker's acceptance of it, and say nothing at
|
|
3228
|
+
// all about its ACL. User mode compares the adopted authority's ACL against the inventory's, so
|
|
3229
|
+
// it refuses a divergence on both fields already, but it refuses it as DRIFT rather than as this
|
|
3230
|
+
// rule, and an inventory whose record and credential agree with each other and disagree with
|
|
3231
|
+
// this rule is exactly the document an operator would not notice.
|
|
3232
|
+
//
|
|
3233
|
+
// Refuses rather than strips. Silently narrowing an admin document would leave the operator
|
|
3234
|
+
// holding a record that says one thing and a mesh that does another, and the whole reason this
|
|
3235
|
+
// is reachable is that nobody reads the record.
|
|
3236
|
+
{
|
|
3237
|
+
const owner = entry.identity.mode === "user" ? entry.identity.owner : DEV_OWNER;
|
|
3238
|
+
// The ACTOR HALF IS THE PRINCIPAL, never the display name. In user mode the row this
|
|
3239
|
+
// document re-arms is keyed by `identity.actor` (it is what the provider adopts below and
|
|
3240
|
+
// what every liveness check reads), and an inventory supplies `name` and `identity.actor`
|
|
3241
|
+
// independently, so judging the channel against `name` would judge it against the half that
|
|
3242
|
+
// does not own the plane.
|
|
3243
|
+
const actor = entry.identity.mode === "user" ? entry.identity.actor : entry.identity.id;
|
|
3244
|
+
const foreign = foreignEventChannels([...(entry.launch.allowSubscribe ?? []), ...(entry.launch.allowPublish ?? [])], owner, actor);
|
|
3245
|
+
if (foreign.length)
|
|
3246
|
+
throw new Error(`retained agent ${entry.name}: its record claims another agent's event channel ` +
|
|
3247
|
+
`(${foreign.join(", ")}). An agent may hold its OWN event plane and no other, because that ` +
|
|
3248
|
+
`plane carries the session's tool inputs and outputs. Remove it from the inventory and ` +
|
|
3249
|
+
`grant the reader out of band instead: ` +
|
|
3250
|
+
`${this.readerRemedy(owner, foreign[0])}`);
|
|
3251
|
+
}
|
|
3103
3252
|
if (entry.identity.mode === "open") {
|
|
3104
3253
|
if (this.auth || this.userMode)
|
|
3105
3254
|
throw new Error(`retained agent ${entry.name} is open-mode but the current manager is authenticated`);
|
|
@@ -3328,7 +3477,7 @@ export class Manager {
|
|
|
3328
3477
|
allowSubscribe: entry.launch.allowSubscribe,
|
|
3329
3478
|
allowPublish: entry.launch.allowPublish,
|
|
3330
3479
|
capabilities: entry.launch.capabilities,
|
|
3331
|
-
|
|
3480
|
+
events: entry.launch.events,
|
|
3332
3481
|
mcpServers,
|
|
3333
3482
|
workspaceRoot: this.workspaceRoot,
|
|
3334
3483
|
});
|
|
@@ -3392,7 +3541,7 @@ export class Manager {
|
|
|
3392
3541
|
allowSubscribe: entry.launch.allowSubscribe,
|
|
3393
3542
|
allowPublish: entry.launch.allowPublish,
|
|
3394
3543
|
capabilities: entry.launch.capabilities,
|
|
3395
|
-
|
|
3544
|
+
events: entry.launch.events,
|
|
3396
3545
|
shareTools: entry.launch.shareTools,
|
|
3397
3546
|
forkSource: entry.launch.forkSource,
|
|
3398
3547
|
},
|
|
@@ -4924,6 +5073,44 @@ export class Manager {
|
|
|
4924
5073
|
}
|
|
4925
5074
|
return { ok: true, data: { name, path } };
|
|
4926
5075
|
}
|
|
5076
|
+
/** The post-authorization `input` effect (C3): type `text` into the seat's terminal as if a human
|
|
5077
|
+
* had. The single call an external UI needs to deliver a harness command (`/compact`, `/clear`,
|
|
5078
|
+
* `/model`) without holding a terminal open on the caller's side.
|
|
5079
|
+
*
|
|
5080
|
+
* Why the runtime HANDLE and not an attach session: a session is a stream (backlog, subscriber
|
|
5081
|
+
* set, a lifetime the session plane accounts for and caps). Opening and discarding one per
|
|
5082
|
+
* keystroke line would burn a session slot for a write, and would put a capacity refusal in the
|
|
5083
|
+
* path of an operation that has no capacity cost. {@link AgentHandle.write} is the one-shot
|
|
5084
|
+
* sibling of `interrupt()`, which already writes into the same pty.
|
|
5085
|
+
*
|
|
5086
|
+
* Three refusals, each for a different reason and each with its own code, so a caller can tell
|
|
5087
|
+
* "never going to work" from "not right now":
|
|
5088
|
+
* - the name was refilled while authorization awaited: act on the incarnation the caller was
|
|
5089
|
+
* authorized for or on nothing at all (the same guard {@link attachAuthorized} carries, for
|
|
5090
|
+
* the same reason: a name is a reusable slot and `authorizeNamed` can await a ledger read);
|
|
5091
|
+
* - the agent is not running: there is no terminal to type into;
|
|
5092
|
+
* - the runtime cannot write (tmux/cmux/orca/herdr attach to an externally-owned process, so
|
|
5093
|
+
* they own no input stream for it): `unimplemented`, named by runtime kind. NOT a fallback
|
|
5094
|
+
* to an attach session and NOT a silent success - a dropped keystroke would leave the caller
|
|
5095
|
+
* believing a command was delivered that never was.
|
|
5096
|
+
*
|
|
5097
|
+
* `enter` defaults to true: a harness command typed but not submitted has not been delivered.
|
|
5098
|
+
* Nothing is echoed back; the caller reads the resulting turns from the event plane. */
|
|
5099
|
+
inputAuthorized(a, args) {
|
|
5100
|
+
if (this.agents.get(a.name) !== a)
|
|
5101
|
+
throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" was replaced during authorization - retry`);
|
|
5102
|
+
if (a.handle.status() !== "running")
|
|
5103
|
+
throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" is not running (${a.handle.status()}); nothing to type into`);
|
|
5104
|
+
const write = a.handle.write?.bind(a.handle);
|
|
5105
|
+
if (!write)
|
|
5106
|
+
throw new EpEnvelopeError("unimplemented", `input is not supported by runtime ${a.handle.kind}`);
|
|
5107
|
+
// The contract validated `text` (non-empty, <= 64KiB) and `enter` (boolean) before this ran, so
|
|
5108
|
+
// the only decision left is the carriage return. `!== false` and not `?? true`: an ABSENT enter
|
|
5109
|
+
// and an explicit `true` must behave identically, and only `false` may suppress the return.
|
|
5110
|
+
const data = `${String(args.text)}${args.enter !== false ? "\r" : ""}`;
|
|
5111
|
+
write(data);
|
|
5112
|
+
return { name: a.name, bytes: Buffer.byteLength(data, "utf8") };
|
|
5113
|
+
}
|
|
4927
5114
|
/** The post-authorization attach effect (P2 item 6): mint the holder-bound §13.6 offer, redeem it
|
|
4928
5115
|
* through the ONE session plane (one-use CAS + presenter-equality), and stand up the PTY bridge —
|
|
4929
5116
|
* atomically. The reply is the SIGNED grant (no ws:// URL, non-bearer, never logged); the caller
|