@platless/axom-cli 0.6.36 → 0.6.52
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/README.md +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationPreparation.js +1 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -1,237 +1,108 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { inspectWorkstation } from '../workstation/control/inspect.js';
|
|
4
|
-
import { runLocalJsonCommand } from '../workstation/control/commands.js';
|
|
5
|
-
import { hostname } from 'node:os';
|
|
6
|
-
import { basename } from 'node:path';
|
|
7
|
-
import { loadConfig, resolveServer, UsageError } from '../config.js';
|
|
8
|
-
import { accessTokenFor } from '../credentials.js';
|
|
9
|
-
import { ensureFreshToken } from '../mcp.js';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { UsageError } from '../config.js';
|
|
10
3
|
import { runCommand } from '../output.js';
|
|
11
|
-
import { clientDir
|
|
12
|
-
import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
|
|
4
|
+
import { clientDir } from '../workstation/config.js';
|
|
13
5
|
import { configureRd, rdStatus } from '../workstation/rd/control.js';
|
|
14
6
|
import { runManagedDesktop } from '../workstation/rd/sessions.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* here once rather than twice.
|
|
23
|
-
*
|
|
24
|
-
* An existing workstation credential boots directly — the common case, and the only sane one for a
|
|
25
|
-
* container that gets recreated on every restart: re-registering each boot would rotate keys for no
|
|
26
|
-
* reason. Otherwise a usable access token registers this machine — `AXOM_ACCESS_TOKEN` (set by a
|
|
27
|
-
* container with no human and no browser) or a stored user token (from `axom auth`), refreshing it
|
|
28
|
-
* first if it's stale. With neither available, an interactive terminal falls back to the device
|
|
29
|
-
* flow; a headless one gets a hard stop naming the fix.
|
|
30
|
-
*/
|
|
31
|
-
async function ensureRegistered(deps, serverFlag, nameFlag) {
|
|
32
|
-
const registered = await loadClientConfig(clientDir()).then(() => true, () => false);
|
|
33
|
-
if (registered)
|
|
34
|
-
return false;
|
|
35
|
-
const config = await loadConfig();
|
|
36
|
-
const server = resolveServer(config, serverFlag);
|
|
37
|
-
let token = await accessTokenFor(server);
|
|
38
|
-
if (!token) {
|
|
39
|
-
if (!deps.isInteractive()) {
|
|
40
|
-
throw new UsageError('No credential for this workstation. Set AXOM_ACCESS_TOKEN (create one under Settings → Security → ' +
|
|
41
|
-
'Access Tokens), or run `axom auth` on a machine with a browser.');
|
|
42
|
-
}
|
|
43
|
-
await deps.authenticate(server);
|
|
44
|
-
token = await ensureFreshToken(server);
|
|
45
|
-
}
|
|
46
|
-
const cwd = process.cwd();
|
|
47
|
-
await deps.registerWorkstation(server, resolveWorkstationName(nameFlag, process.env, hostname()), token, {
|
|
48
|
-
name: sanitizeName(basename(cwd), 'default'),
|
|
49
|
-
cwd,
|
|
50
|
-
shell: process.env.SHELL ?? '/bin/bash',
|
|
51
|
-
});
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Removes `AXOM_ACCESS_TOKEN` from *this process's* copy of the environment, right before the bare
|
|
56
|
-
* `axom workstation` command becomes a long-lived daemon.
|
|
57
|
-
*
|
|
58
|
-
* The daemon spawns agents and shell commands with `{ ...process.env, ... }` (exec.ts, acpClient.ts),
|
|
59
|
-
* so anything still in here reaches every model-authored command and every dependency postinstall
|
|
60
|
-
* that runs in `/workspace` — a container is precisely where this product runs untrusted code. What
|
|
61
|
-
* would leak is not the workstation's own key pair (scoped to one workstation, on disk) but a
|
|
62
|
-
* credential that authenticates *as the user*: it can register further workstations under their
|
|
63
|
-
* account and, with business scopes, call granted MCP tools as them, until somebody notices and
|
|
64
|
-
* revokes it. Fixed once here rather than filtered at each of the two spawn sites, so the next spawn site
|
|
65
|
-
* inherits the fix instead of having to remember it.
|
|
66
|
-
*
|
|
67
|
-
* Nothing after this point needs the value. The daemon authenticates with the file credential
|
|
68
|
-
* written at registration (`clientKey`/`serverPub`), and the `container` update lifecycle downloads
|
|
69
|
-
* its release over that same authenticated management channel.
|
|
70
|
-
*
|
|
71
|
-
* Unconditional, not gated on "we just registered": a container that still has its `/data/ws` volume
|
|
72
|
-
* boots straight past `ensureRegistered`, and that is exactly the long-lived container this protects.
|
|
73
|
-
*
|
|
74
|
-
* The token stays in the *container's* environment, so a restarted container still registers — only
|
|
75
|
-
* this process's copy is dropped. Every reader of it lives before this line: `accessTokenFor`
|
|
76
|
-
* (credentials.ts) reads `process.env.AXOM_ACCESS_TOKEN` on its first line, and its only caller here
|
|
77
|
-
* is `ensureRegistered` above; `insufficientScopeError`/`envAccessTokenRejectedError` (mcp.ts) read
|
|
78
|
-
* it too, but only from the one-shot `axom execute`/`axom struct` CLI processes, which are separate
|
|
79
|
-
* processes with their own untouched copy.
|
|
80
|
-
*
|
|
81
|
-
* `workstation start` deliberately does not call this: it registers and hands off to launchd/systemd
|
|
82
|
-
* rather than becoming the daemon, so dropping the variable would affect only a CLI process that is
|
|
83
|
-
* about to exit.
|
|
84
|
-
*/
|
|
85
|
-
function dropAccessTokenFromEnvironment() {
|
|
86
|
-
delete process.env.AXOM_ACCESS_TOKEN;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Registers `axom workstation start|stop|restart|status|install|uninstall`, the bare
|
|
90
|
-
* `axom workstation` (Docker ENTRYPOINT / debugging foreground run), and the hidden top-level
|
|
91
|
-
* `axom __daemon`.
|
|
92
|
-
*
|
|
93
|
-
* `__daemon` is registered on `program` itself, not on the `workstation` subcommand group: the
|
|
94
|
-
* generated launchd/systemd service definitions invoke it as `<cliPath> __daemon`, a bare
|
|
95
|
-
* top-level argument (see service/launchd.ts and service/systemd.ts).
|
|
96
|
-
*/
|
|
7
|
+
import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
8
|
+
import { readLocalRequest } from '../workstation/control/commands.js';
|
|
9
|
+
import { LocalControlError } from '../workstation/control/protocol.js';
|
|
10
|
+
import { deviceEventSchema } from '../workstation/control/deviceProtocol.js';
|
|
11
|
+
import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
|
|
12
|
+
import { registrationIdSchema } from '../workstation/registrations/model.js';
|
|
13
|
+
import { addWorkstationRegistration, listWorkstationRegistrations, removeWorkstationRegistration } from './workstationRegistrations.js';
|
|
97
14
|
export function registerWorkstationCommands(program, deps) {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
// Same idiom as `registerUpdateCommand`: the global `--server` is read at action time, not at
|
|
101
|
-
// registration time, so `axom --server <url> workstation start` works. `axom auth` only writes
|
|
102
|
-
// `defaultServer` when it is unset, so a user who authenticated against localhost first and
|
|
103
|
-
// production second would otherwise have no way to keep `start` off localhost.
|
|
15
|
+
const workstation = program.command('workstation').description('Manage this device and its workstation registration collection');
|
|
16
|
+
const registrations = deps.registrations ?? { add: addWorkstationRegistration, list: listWorkstationRegistrations, remove: removeWorkstationRegistration };
|
|
104
17
|
const serverOption = () => program.opts().server;
|
|
105
|
-
// `workstation` (this group) and its `start` subcommand both declare `--server`/`--name` — the
|
|
106
|
-
// group needs its own copy so the *bare* command accepts them, and `start` keeps its copy for its
|
|
107
|
-
// own `--help`. But for `axom workstation start --server/--name <value>`, only one of the two
|
|
108
|
-
// declarations ever actually captures the flag, and it's not always the same one: `program`
|
|
109
|
-
// parses its own options over the *entire* remaining argv before `workstation` gets a look, so
|
|
110
|
-
// `--server` — also declared on `program`, globally — is always captured there, leaving both
|
|
111
|
-
// `options.server` and `workstationOption().server` dead; `--name` has no `program`-level
|
|
112
|
-
// declaration, so it falls through to `workstation`, which *does* capture it, leaving `start`'s
|
|
113
|
-
// own `options.name` the dead one instead. `start`'s action below reads the whole fallback chain
|
|
114
|
-
// so it doesn't matter which link is live for a given flag.
|
|
115
|
-
const workstationOption = () => workstation.opts();
|
|
116
18
|
const applyRd = deps.configureRd ?? configureRd;
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (options.json) {
|
|
133
|
-
if (
|
|
134
|
-
throw new UsageError('--
|
|
135
|
-
|
|
19
|
+
const rejectSelectors = (options = {}) => {
|
|
20
|
+
const group = workstation.opts();
|
|
21
|
+
if (options.server !== undefined || options.name !== undefined || group.server !== undefined || group.name !== undefined || serverOption() !== undefined)
|
|
22
|
+
throw new UsageError('Workstation lifecycle commands apply to the whole device. Use `axom workstation add [address]` to add a server/account registration, or `axom workstation remove <registrationId>` to remove one.');
|
|
23
|
+
};
|
|
24
|
+
const removed = () => runCommand(async () => { throw new UsageError('This workstation command was removed. Use `axom workstation add [address]`, `list`, or `remove <registrationId>`; start/stop operate on the whole device.'); });
|
|
25
|
+
const dispatch = async (action, options, task, registrationId) => {
|
|
26
|
+
const target = registrationIdSchema.safeParse(registrationId);
|
|
27
|
+
const operation = { operationId: randomUUID(), registrationId: target.success ? target.data : null };
|
|
28
|
+
const captureOperationId = (value) => {
|
|
29
|
+
const parsed = operationIdSchema.safeParse(value);
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid workstation operation correlation ID');
|
|
32
|
+
operation.operationId = parsed.data;
|
|
33
|
+
};
|
|
34
|
+
if (!options.json) {
|
|
35
|
+
if (options.inputStdin)
|
|
36
|
+
throw new UsageError('--input-stdin requires --json');
|
|
37
|
+
if (options.operationId !== undefined)
|
|
38
|
+
captureOperationId(options.operationId);
|
|
39
|
+
await task(operation);
|
|
136
40
|
return;
|
|
137
41
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
workstation
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
.
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.description('Install and enable the operating-system service')
|
|
187
|
-
.action((options) => runCommand(async () => { if (options.json) {
|
|
188
|
-
if (!options.inputStdin)
|
|
189
|
-
throw new UsageError('--json requires --input-stdin');
|
|
190
|
-
await localJson('install');
|
|
191
|
-
}
|
|
192
|
-
else
|
|
193
|
-
await withWorkstationLock(clientDir(), 'operation', async () => { await recoverStoppedIdentity(clientDir()); await assertIdentityCanStart(clientDir()); await deps.install(); }); }));
|
|
194
|
-
workstation
|
|
195
|
-
.command('uninstall')
|
|
196
|
-
.description('Disable and remove the operating-system service')
|
|
197
|
-
.action(() => runCommand(async () => { await withWorkstationLock(clientDir(), 'operation', () => deps.uninstall()); }));
|
|
198
|
-
workstation.command('inspect').requiredOption('--json', 'Emit local management protocol JSON')
|
|
199
|
-
.action(() => runCommand(async () => { process.stdout.write(JSON.stringify(await inspectWorkstation(clientDir())) + '\n'); }));
|
|
200
|
-
workstation.command('set-enabled').description('Persistently enable or disable this workstation service')
|
|
201
|
-
.requiredOption('--json').requiredOption('--input-stdin').action(() => localJson('set-enabled'));
|
|
202
|
-
for (const verb of ['register', 'forget', 'begin-remove', 'adopt-owner', 'recover']) {
|
|
203
|
-
workstation.command(verb, { hidden: verb === 'forget' }).requiredOption('--json', 'Emit local management protocol JSON')
|
|
204
|
-
.requiredOption('--input-stdin', 'Read the guarded request from stdin').action(() => localJson(verb));
|
|
42
|
+
try {
|
|
43
|
+
if (options.operationId !== undefined)
|
|
44
|
+
captureOperationId(options.operationId);
|
|
45
|
+
if (!options.inputStdin) {
|
|
46
|
+
await task(operation);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const request = await readLocalRequest(process.stdin);
|
|
50
|
+
if (request && typeof request === 'object' && 'operationId' in request)
|
|
51
|
+
captureOperationId(request.operationId);
|
|
52
|
+
if (!request || typeof request !== 'object' || !('action' in request) || request.action !== action)
|
|
53
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Use a matching v2 device command request');
|
|
54
|
+
if (registrationId !== undefined && (!('guard' in request) || !request.guard || typeof request.guard !== 'object' || !('registrationId' in request.guard) || request.guard.registrationId !== registrationId))
|
|
55
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The guarded removal must match the exact registration ID');
|
|
56
|
+
const result = await executeDeviceCommand(request, clientDir());
|
|
57
|
+
if (result.operationId !== operation.operationId)
|
|
58
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original workstation operation reply does not match');
|
|
59
|
+
deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
|
|
60
|
+
if (result.status !== 'succeeded')
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const code = error instanceof LocalControlError ? error.code : 'SERVICE_FAILED';
|
|
65
|
+
deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: operation.operationId, result: { operationId: operation.operationId, registrationId: operation.registrationId, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : ['BINDING_CONFLICT', 'OWNER_MISMATCH'].includes(code) ? 'conflict' : 'failed', inspection: null, error: { code, message: error instanceof LocalControlError ? error.message : 'The workstation operation could not be confirmed' } } })));
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
workstation.command('list').description('List local workstation registrations and device state').option('--json', 'Emit the v2 device collection').action(options => runCommand(() => dispatch('inspect', options, () => registrations.list(options))));
|
|
70
|
+
workstation.command('add [address]').description('Add an exact server/account registration without changing ordinary CLI login').option('--name <name>', 'Workstation name').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a protected v2 enrollment request from stdin').option('--operation-id <id>', 'Operation correlation ID').option('--owner <userId>', 'Require this account identity')
|
|
71
|
+
.action((address, options) => runCommand(() => dispatch('add-registration', options, operation => registrations.add({ ...options, operation, name: options.name ?? workstation.opts().name, server: address ?? serverOption(), expectedOwnerUserId: options.owner }))));
|
|
72
|
+
workstation.command('remove <registrationId>').description('Remove this exact local registration; busy resources require explicit --force').option('--force', 'Retire this registration’s resources').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read the original guarded v2 removal request from stdin').option('--operation-id <id>', 'Retry the original operation ID')
|
|
73
|
+
.action((id, options) => runCommand(() => dispatch('remove-registration', options, operation => registrations.remove(id, { ...options, operation }), id)));
|
|
74
|
+
for (const verb of ['start', 'stop', 'restart', 'install', 'uninstall']) {
|
|
75
|
+
const command = workstation.command(verb).description(`${verb} the whole-device service`).option('--name <name>', 'Removed selector; use workstation add').option('--server <url>', 'Removed selector; use workstation add').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a guarded v2 whole-device request from stdin');
|
|
76
|
+
if (verb === 'start')
|
|
77
|
+
command.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display');
|
|
78
|
+
command.action(options => runCommand(async () => {
|
|
79
|
+
rejectSelectors(options);
|
|
80
|
+
const rdOptions = { ...workstation.opts(), ...options };
|
|
81
|
+
const hasRd = rdOptions.enableRd || rdOptions.disableRd || rdOptions.rdSession || rdOptions.rdDisplay;
|
|
82
|
+
if (hasRd && (verb !== 'start' || options.json))
|
|
83
|
+
throw new UsageError('RD flags require a plain `axom workstation start` or foreground command');
|
|
84
|
+
if (options.json && !options.inputStdin)
|
|
85
|
+
throw new UsageError('--json requires --input-stdin for device lifecycle commands');
|
|
86
|
+
if (hasRd)
|
|
87
|
+
await applyRd(clientDir(), rdOptions);
|
|
88
|
+
await dispatch(verb, options, () => deps[verb]());
|
|
89
|
+
}));
|
|
205
90
|
}
|
|
206
|
-
|
|
91
|
+
workstation.command('status').description('Report the whole-device service status').action(() => runCommand(async () => { rejectSelectors(); deps.log(await deps.status()); }));
|
|
92
|
+
workstation.command('inspect').requiredOption('--json', 'Emit the v2 device collection').action(() => runCommand(async () => { deps.log(JSON.stringify(await inspectLocalDevice(clientDir()))); }));
|
|
93
|
+
for (const verb of ['auth', 'register', 'forget', 'begin-remove', 'adopt-owner', 'recover', 'set-enabled'])
|
|
94
|
+
workstation.command(verb, { hidden: true }).allowUnknownOption().allowExcessArguments().action(removed);
|
|
95
|
+
const rd = workstation.command('rd').description('Manage Linux remote desktop');
|
|
207
96
|
rd.command('status').action(() => runCommand(async () => { deps.log(JSON.stringify(await rdStatus(clientDir()), null, 2)); }));
|
|
208
|
-
rd.command('disable').action(() => runCommand(
|
|
209
|
-
workstation
|
|
210
|
-
.option('--
|
|
211
|
-
.option('--name <name>', 'Workstation name (default: AXOM_WORKSTATION_NAME, then the hostname)')
|
|
212
|
-
.option('--enable-rd', 'Install and enable Linux remote desktop')
|
|
213
|
-
.option('--disable-rd', 'Disable remote desktop without changing workstation registration')
|
|
214
|
-
.option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual')
|
|
215
|
-
.option('--rd-display <display>', 'Choose an existing local X11 display, such as :10')
|
|
216
|
-
.description('Run the workstation daemon in the foreground (Docker ENTRYPOINT / debugging); subcommands manage the OS service')
|
|
97
|
+
rd.command('disable').action(() => runCommand(() => applyRd(clientDir(), { disableRd: true })));
|
|
98
|
+
workstation.option('--server <url>', 'Removed registration shortcut; use workstation add').option('--name <name>', 'Removed registration shortcut; use workstation add')
|
|
99
|
+
.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display')
|
|
217
100
|
.action((options) => runCommand(async () => {
|
|
218
|
-
|
|
219
|
-
await
|
|
220
|
-
|
|
101
|
+
rejectSelectors(options);
|
|
102
|
+
await applyRd(clientDir(), options);
|
|
103
|
+
delete process.env.AXOM_ACCESS_TOKEN;
|
|
221
104
|
await deps.runDaemon({ supervision: deps.detectSupervision() });
|
|
222
105
|
}));
|
|
223
|
-
program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>')
|
|
224
|
-
|
|
225
|
-
program.command('__daemon', { hidden: true }).action(() => runCommand(async () => {
|
|
226
|
-
// The literal exec target launchd/systemd start: the one call site allowed to opt into
|
|
227
|
-
// OS-service treatment. See RunDaemonOptions in daemon.ts for why this must be an explicit
|
|
228
|
-
// flag rather than "no `supervision` was passed".
|
|
229
|
-
//
|
|
230
|
-
// Same reasoning as the bare command's call: this process is about to become the daemon that
|
|
231
|
-
// spawns agents and shell commands with its own environment. A service definition is not
|
|
232
|
-
// supposed to carry AXOM_ACCESS_TOKEN at all, which makes this usually a no-op — and exactly
|
|
233
|
-
// why it belongs here rather than being left to whoever writes the next plist.
|
|
234
|
-
dropAccessTokenFromEnvironment();
|
|
235
|
-
await deps.runDaemon({ supervisedByServiceManager: true });
|
|
236
|
-
}));
|
|
106
|
+
program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>').action((options) => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await runManagedDesktop(options.directory); }));
|
|
107
|
+
program.command('__daemon', { hidden: true }).action(() => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await deps.runDaemon({ supervisedByServiceManager: true }); }));
|
|
237
108
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { peekAccessToken, revokeRefreshToken } from '../credentials.js';
|
|
6
|
+
import { resolveAuthServer } from './auth.js';
|
|
7
|
+
import { clientDir } from '../workstation/config.js';
|
|
8
|
+
import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
9
|
+
import { deviceEventSchema, deviceResultSchema, parseDeviceInspection } from '../workstation/control/deviceProtocol.js';
|
|
10
|
+
import { LocalControlError } from '../workstation/control/protocol.js';
|
|
11
|
+
import { normalizeWorkstationServer } from '../workstation/control/serverIdentity.js';
|
|
12
|
+
import { verifyRegistrationAuthority } from '../workstation/registrations/enrollment.js';
|
|
13
|
+
import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
|
|
14
|
+
import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
|
|
15
|
+
import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
|
|
16
|
+
import { registrationIdSchema } from '../workstation/registrations/model.js';
|
|
17
|
+
const defaults = {
|
|
18
|
+
inspect: () => inspectLocalDevice(clientDir()), execute: request => executeDeviceCommand(request, clientDir()),
|
|
19
|
+
authority: verifyRegistrationAuthority, authorize: authorizeDevice, revoke: revokeRefreshToken, log: console.log,
|
|
20
|
+
};
|
|
21
|
+
export async function resolveWorkstationAddServer(explicit) {
|
|
22
|
+
return resolveAuthServer(await loadConfig(), explicit);
|
|
23
|
+
}
|
|
24
|
+
function printResult(result, options, log) {
|
|
25
|
+
result = deviceResultSchema.parse(result);
|
|
26
|
+
if (options.json)
|
|
27
|
+
log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
|
|
28
|
+
else
|
|
29
|
+
log(`${result.status}: ${result.registrationId ?? 'device'} (operation ${result.operationId})${result.error ? ` — ${result.error.code}: ${result.error.message}` : ''}`);
|
|
30
|
+
if (result.status !== 'succeeded')
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
export async function listWorkstationRegistrations(options = {}, overrides = {}) {
|
|
34
|
+
const deps = { ...defaults, ...overrides };
|
|
35
|
+
const inspection = parseDeviceInspection(await deps.inspect());
|
|
36
|
+
if (options.json) {
|
|
37
|
+
deps.log(JSON.stringify(inspection));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
deps.log(`Workstation device: ${inspection.runtime.state}`);
|
|
41
|
+
if (!inspection.registrations.length)
|
|
42
|
+
deps.log('No workstation registrations. Use `axom workstation add [address]`.');
|
|
43
|
+
for (const registration of inspection.registrations)
|
|
44
|
+
deps.log(`${registration.id}\t${registration.server}\taccount=${registration.ownerUserId}\tworkstation=${registration.workstationId}\t${registration.connection}\t${registration.identityState}\t${registration.removalState}`);
|
|
45
|
+
for (const pending of inspection.pending)
|
|
46
|
+
deps.log(`pending: ${pending.id}\t${pending.kind}\t${pending.registrationId ?? 'device'}\t${pending.phase}\t${pending.outcome}`);
|
|
47
|
+
}
|
|
48
|
+
export async function addWorkstationRegistration(options = {}, overrides = {}) {
|
|
49
|
+
const deps = { ...defaults, ...overrides };
|
|
50
|
+
const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
|
|
51
|
+
const server = normalizeWorkstationServer(await resolveWorkstationAddServer(options.server));
|
|
52
|
+
const before = parseDeviceInspection(await deps.inspect());
|
|
53
|
+
if (before.runtime.state === 'unknown')
|
|
54
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Verify the workstation owner before adding a registration');
|
|
55
|
+
const operationId = operationIdSchema.parse(operation.operationId);
|
|
56
|
+
const hasExplicitAccessToken = Boolean(process.env.AXOM_ACCESS_TOKEN?.trim());
|
|
57
|
+
let token = await peekAccessToken(server);
|
|
58
|
+
let ownerUserId;
|
|
59
|
+
if (token) {
|
|
60
|
+
try {
|
|
61
|
+
ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!(error instanceof LocalControlError))
|
|
65
|
+
throw error;
|
|
66
|
+
if (error.code !== 'AUTHORIZATION_REQUIRED' && (error.code !== 'OWNER_MISMATCH' || hasExplicitAccessToken))
|
|
67
|
+
throw error;
|
|
68
|
+
token = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let issued;
|
|
72
|
+
try {
|
|
73
|
+
if (!token) {
|
|
74
|
+
if (!options.json)
|
|
75
|
+
deps.log('Workstation permission needs a separate approval; unavailable or expired ordinary access credentials are left unchanged.');
|
|
76
|
+
const host = hostname();
|
|
77
|
+
issued = await deps.authorize(server, `Axomwork workstation (${host})`, host, { print: message => { process.stderr.write(message + '\n'); } }, ['workstation'], Object.freeze({ deviceId: `workstation-${randomUUID()}` }));
|
|
78
|
+
token = issued.accessToken;
|
|
79
|
+
ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
|
|
80
|
+
}
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const result = await deps.execute({ schemaVersion: 2, action: 'add-registration', operationId,
|
|
83
|
+
guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation },
|
|
84
|
+
input: { operationId, server, accessToken: token, expectedOwnerUserId: ownerUserId, defaults: { name: resolveWorkstationName(options.name, process.env, hostname()), shell: process.env.SHELL ?? '/bin/sh', workspace: { name: sanitizeName(basename(cwd), 'default'), cwd, shell: process.env.SHELL ?? '/bin/sh' } } },
|
|
85
|
+
});
|
|
86
|
+
printResult(result, options, deps.log);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (issued)
|
|
90
|
+
await deps.revoke(server, issued.refreshToken);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function removeWorkstationRegistration(id, options = {}, overrides = {}) {
|
|
94
|
+
const deps = { ...defaults, ...overrides };
|
|
95
|
+
const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
|
|
96
|
+
const target = registrationIdSchema.safeParse(id);
|
|
97
|
+
if (!target.success) {
|
|
98
|
+
operation.registrationId = null;
|
|
99
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Invalid registration ID; use `axom workstation list`');
|
|
100
|
+
}
|
|
101
|
+
operation.registrationId = target.data;
|
|
102
|
+
const before = parseDeviceInspection(await deps.inspect());
|
|
103
|
+
const registration = before.registrations.find(candidate => candidate.id === id);
|
|
104
|
+
if (!registration)
|
|
105
|
+
throw new LocalControlError('BINDING_CONFLICT', 'No registration matches that exact ID; use `axom workstation list`');
|
|
106
|
+
const pending = before.pending.find(operation => operation.kind === 'remove' && operation.registrationId === id);
|
|
107
|
+
const operationId = operationIdSchema.parse(options.operationId ?? pending?.id ?? operation.operationId);
|
|
108
|
+
operation.operationId = operationId;
|
|
109
|
+
if (before.runtime.state !== 'running' || before.runtime.generation === null) {
|
|
110
|
+
printResult({ operationId, registrationId: id, status: 'pending', inspection: before, error: { code: 'OUTCOME_UNKNOWN', message: pending ? 'Original removal authority is unavailable; preserve its pending operation and data for verification' : 'Removal needs the authenticated running device owner; start the device, inspect, and retry' } }, options, deps.log);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
printResult(await deps.execute({ schemaVersion: 2, action: 'remove-registration', operationId, force: options.force === true,
|
|
114
|
+
guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation, registrationId: id, registrationRevision: registration.revision },
|
|
115
|
+
}), options, deps.log);
|
|
116
|
+
}
|
package/dist/credentialBroker.js
CHANGED
|
@@ -30,9 +30,17 @@ export async function requestCredentialBroker(request) {
|
|
|
30
30
|
return await sendRequest(brokerRequest, controller.signal);
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
|
-
if (error instanceof BrokerOperationError)
|
|
33
|
+
if (error instanceof BrokerOperationError) {
|
|
34
|
+
if (request.action === 'peek' && error.message === 'Unsupported credential broker operation.')
|
|
35
|
+
return { ok: true, version: PROTOCOL_VERSION };
|
|
34
36
|
throw error;
|
|
37
|
+
}
|
|
35
38
|
throwIfAborted(controller.signal);
|
|
39
|
+
if (request.action === 'peek') {
|
|
40
|
+
if (['ENOENT', 'ECONNREFUSED'].includes(error.code ?? ''))
|
|
41
|
+
return { ok: true, version: PROTOCOL_VERSION };
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
36
44
|
await installAndStartBroker(controller.signal);
|
|
37
45
|
let lastError;
|
|
38
46
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
@@ -129,6 +137,9 @@ async function handleRequest(raw, connection, credentials) {
|
|
|
129
137
|
const accessToken = await credentials.accessTokenFromNativeStore(request.server, request.forceRefresh);
|
|
130
138
|
response = { ok: true, accessToken, version: PROTOCOL_VERSION };
|
|
131
139
|
}
|
|
140
|
+
else if (request.action === 'peek') {
|
|
141
|
+
response = { ok: true, accessToken: credentials.peekNativeAccessToken(request.server), version: PROTOCOL_VERSION };
|
|
142
|
+
}
|
|
132
143
|
else if (request.action === 'revoke') {
|
|
133
144
|
const persisted = await credentials.loadNativeTokens(request.server);
|
|
134
145
|
if (persisted?.refreshToken)
|
|
@@ -319,7 +330,7 @@ function parseRequest(raw) {
|
|
|
319
330
|
const request = JSON.parse(raw);
|
|
320
331
|
if (request.version !== PROTOCOL_VERSION)
|
|
321
332
|
throw new Error('Unsupported credential broker protocol version.');
|
|
322
|
-
if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke') {
|
|
333
|
+
if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke' && request.action !== 'peek') {
|
|
323
334
|
throw new Error('Unsupported credential broker operation.');
|
|
324
335
|
}
|
|
325
336
|
if (typeof request.server !== 'string' || !request.server)
|
package/dist/credentials.js
CHANGED
|
@@ -34,6 +34,22 @@ export async function accessTokenFor(server, forceRefresh = false) {
|
|
|
34
34
|
}
|
|
35
35
|
return accessTokenFromNativeStore(server, forceRefresh);
|
|
36
36
|
}
|
|
37
|
+
export function peekNativeAccessToken(server) {
|
|
38
|
+
const cached = accessCache.get(server);
|
|
39
|
+
return cached?.accessToken && Date.parse(cached.expiresAt) - Date.now() >= 30_000 ? cached.accessToken : undefined;
|
|
40
|
+
}
|
|
41
|
+
export async function peekAccessToken(server) {
|
|
42
|
+
const fromEnv = process.env.AXOM_ACCESS_TOKEN?.trim();
|
|
43
|
+
if (fromEnv)
|
|
44
|
+
return fromEnv;
|
|
45
|
+
if (usesTestMemoryStore()) {
|
|
46
|
+
const cached = memoryStore.get(server);
|
|
47
|
+
return cached?.accessToken && Date.parse(cached.expiresAt) - Date.now() >= 30_000 ? cached.accessToken : undefined;
|
|
48
|
+
}
|
|
49
|
+
if (process.platform === 'darwin')
|
|
50
|
+
return (await requestCredentialBroker({ action: 'peek', server })).accessToken;
|
|
51
|
+
return peekNativeAccessToken(server);
|
|
52
|
+
}
|
|
37
53
|
export async function clearTokens(server) {
|
|
38
54
|
if (usesTestMemoryStore()) {
|
|
39
55
|
memoryStore.delete(server);
|
|
@@ -41,8 +41,8 @@ $ARGUMENTS
|
|
|
41
41
|
Workflow:
|
|
42
42
|
1. Find the operation on its model (\`axom__app_struct__get_model\`) or the service by name.
|
|
43
43
|
2. Call \`axom__execute__run_operation\` (applicationName/modelName/operationName/input) or \`axom__execute__run_service\` (serviceName/input).
|
|
44
|
-
3. Interpret the ExecuteResult payload: \`status\` (success/failure/running), \`message\`, \`text\`, \`returnValue\`, \`
|
|
45
|
-
4. When \`status\` is \`running\`, follow \`
|
|
44
|
+
3. Interpret the ExecuteResult payload: \`status\` (success/failure/running), \`message\`, \`text\`, \`returnValue\`, \`workId\`/\`workKind\`, \`errors[]\` ({target,code,message}).
|
|
45
|
+
4. When \`status\` is \`running\`, follow \`workId\` (see \`workKind\`) until a terminal state before reporting the outcome.
|
|
46
46
|
|
|
47
47
|
For sync-vs-job semantics see knowledge page \`concepts/execution/synchronous-and-task-execution.md\`; for run following see knowledge page \`agent-integration/operating-patterns/following-runs.md\`.
|
|
48
48
|
`,
|