@prohost/cli 0.12.4 → 0.13.2
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/CHANGELOG.md +54 -0
- package/README.md +27 -3
- package/dist/agent/api.d.ts +15 -7
- package/dist/agent/api.js +6 -1
- package/dist/agent/claude.d.ts +7 -0
- package/dist/agent/claude.js +4 -0
- package/dist/agent/contract.d.ts +37 -0
- package/dist/agent/contract.js +18 -0
- package/dist/agent/exec.d.ts +6 -0
- package/dist/agent/exec.js +5 -1
- package/dist/agent/heartbeat.d.ts +13 -0
- package/dist/agent/heartbeat.js +9 -1
- package/dist/agent/run.js +407 -151
- package/dist/agent/runtime_status.d.ts +6 -0
- package/dist/agent/runtime_status.js +5 -0
- package/dist/agent/venue.d.ts +163 -0
- package/dist/agent/venue.js +272 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -206,6 +206,12 @@ export interface RuntimeStatusReporterOptions {
|
|
|
206
206
|
accountKeys?: () => string[] | undefined;
|
|
207
207
|
/** A run is executing — it will bring fresh numbers, so don't probe beside it. */
|
|
208
208
|
isBusy?: () => boolean;
|
|
209
|
+
/**
|
|
210
|
+
* What this harness can do that the server needs to know before sending it
|
|
211
|
+
* work — today only `venue_run_token` (see `venue.ts`). Read for every
|
|
212
|
+
* frame, so a downgraded harness stops declaring what it no longer does.
|
|
213
|
+
*/
|
|
214
|
+
capabilities?: () => string[];
|
|
209
215
|
now?: () => number;
|
|
210
216
|
/** Timer seam for tests. Returns a cancel function. */
|
|
211
217
|
schedule?: (fn: () => void, ms: number) => () => void;
|
|
@@ -472,6 +472,10 @@ function asOf(window, nowMs) {
|
|
|
472
472
|
return window;
|
|
473
473
|
return { ...window, used_percent: 0, resets_at: null };
|
|
474
474
|
}
|
|
475
|
+
/** Left out when there is nothing to declare, so an older server sees the frame it knows. */
|
|
476
|
+
function capabilitiesField(capabilities) {
|
|
477
|
+
return capabilities && capabilities.length > 0 ? { capabilities } : {};
|
|
478
|
+
}
|
|
475
479
|
function defaultSchedule(fn, ms) {
|
|
476
480
|
const timer = setTimeout(fn, ms);
|
|
477
481
|
timer.unref?.();
|
|
@@ -595,6 +599,7 @@ export class RuntimeStatusReporter {
|
|
|
595
599
|
plan_label: this.state.plan_label ?? null,
|
|
596
600
|
quota_windows: windows,
|
|
597
601
|
observed_at: new Date(nowMs).toISOString(),
|
|
602
|
+
...capabilitiesField(this.options.capabilities?.()),
|
|
598
603
|
// Optional additions: an older server ignores keys it doesn't know.
|
|
599
604
|
...(this.accounts?.current ? { account: this.accounts.current } : {}),
|
|
600
605
|
// Never a list the registry has moved on from — the server would delete
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs that answer in a connected team's thread — a *venue*.
|
|
3
|
+
*
|
|
4
|
+
* Connected Teams let two ProhostAI workspaces talk in each other's team chat,
|
|
5
|
+
* so this agent can be addressed in a thread that belongs to the OTHER
|
|
6
|
+
* workspace. That thread's text is someone else's prompt, and the harness's own
|
|
7
|
+
* credential can write anywhere in this agent's home workspace. So a venue run
|
|
8
|
+
* is answered by a session that holds none of it:
|
|
9
|
+
*
|
|
10
|
+
* * **Its own credential.** The event carries no thread text at all. The
|
|
11
|
+
* harness opens the run's *venue session* over its own `/ws/webhooks` socket
|
|
12
|
+
* (there is no HTTP route to it) and gets back a token minted for this one
|
|
13
|
+
* run together with the thread. The token reads and posts in that one thread and heartbeats,
|
|
14
|
+
* streams and completes that one run — nothing else — and the server refuses
|
|
15
|
+
* the harness's key for all of it. Every request the run makes carries the
|
|
16
|
+
* token. It lives in memory only: never logged, never written to disk.
|
|
17
|
+
* * **No tools, no configuration.** Claude Code is started from the operator's
|
|
18
|
+
* binary and model only, with `--tools ''` (no built-in tools, so nothing can
|
|
19
|
+
* read the credentials file or the environment), `--safe-mode` and
|
|
20
|
+
* `--setting-sources '' --settings '{}'` (no hooks, plugins, skills,
|
|
21
|
+
* `CLAUDE.md`, permissions or settings from `~/.claude` or any project), an
|
|
22
|
+
* empty strict MCP config (never the ProhostAI server or the operator's extra
|
|
23
|
+
* servers), and `--no-session-persistence` (the connected team's text is not
|
|
24
|
+
* written to a transcript). The agent answers from the thread; the harness
|
|
25
|
+
* posts the reply.
|
|
26
|
+
* * **No home environment.** The child gets an allowlisted environment —
|
|
27
|
+
* what a shell and the Claude login need, nothing of ProhostAI's — and an
|
|
28
|
+
* empty directory of its own, removed afterwards.
|
|
29
|
+
*
|
|
30
|
+
* Only Claude Code can be held to that, and only a release that has every one
|
|
31
|
+
* of those flags, so only such a harness declares
|
|
32
|
+
* {@link VENUE_RUN_TOKEN_CAPABILITY} — and the server sends venue runs only to
|
|
33
|
+
* a runtime that declared it. A venue run that reaches any other harness anyway
|
|
34
|
+
* is refused without being run.
|
|
35
|
+
*/
|
|
36
|
+
import type { ApiOptions } from './api.js';
|
|
37
|
+
import type { AgentRunRequest } from './contract.js';
|
|
38
|
+
/**
|
|
39
|
+
* What a harness declares in its `runtime_status` frame when it runs venue
|
|
40
|
+
* sessions as described above. Mirrors `VENUE_RUN_TOKEN_CAPABILITY` in the
|
|
41
|
+
* server's `shared/agents/runtime_status.py`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const VENUE_RUN_TOKEN_CAPABILITY = "venue_run_token";
|
|
44
|
+
/** The token prefix the server mints. Anything else is not a run token. */
|
|
45
|
+
export declare const VENUE_RUN_TOKEN_PREFIX = "pvr_";
|
|
46
|
+
/** Whether a run answers in a connected team's thread. */
|
|
47
|
+
export declare function isVenueRun(run: AgentRunRequest): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The API options a run's requests go out with: its own token for a venue run
|
|
50
|
+
* whose session is open, the harness's credential for every other.
|
|
51
|
+
*/
|
|
52
|
+
export declare function apiForRun(api: ApiOptions, run: AgentRunRequest): ApiOptions;
|
|
53
|
+
/**
|
|
54
|
+
* Whether this Claude Code binary has every flag a venue session depends on.
|
|
55
|
+
*
|
|
56
|
+
* Asked of `--help` rather than inferred from a version number: the flags are
|
|
57
|
+
* what matters, and a build without one of them would reject the command
|
|
58
|
+
* outright — or, worse, a future one might accept and ignore it. Windows is
|
|
59
|
+
* left out because its shell quoting cannot be relied on to hand `--tools` an
|
|
60
|
+
* empty value.
|
|
61
|
+
*/
|
|
62
|
+
export declare function claudeSupportsVenueSessions(helpText: string, platform?: NodeJS.Platform): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The Claude Code command a venue run is answered with.
|
|
65
|
+
*
|
|
66
|
+
* Built from the operator's binary and model only — never the rest of their
|
|
67
|
+
* `--exec` string, whose flags (`--add-dir`, `--mcp-config`, `--allowedTools`,
|
|
68
|
+
* `--dangerously-skip-permissions`) are exactly what a venue session must not
|
|
69
|
+
* get.
|
|
70
|
+
*/
|
|
71
|
+
export declare function claudeVenueCommand(exec: string, model?: string, platform?: NodeJS.Platform): string;
|
|
72
|
+
/**
|
|
73
|
+
* The environment a venue session's child gets, built up rather than pared
|
|
74
|
+
* down: the shell basics above, the locale, and Claude Code's own login and
|
|
75
|
+
* configuration (`CLAUDE_*`, `ANTHROPIC_*` — the brain cannot start without
|
|
76
|
+
* them). Everything else is dropped — ProhostAI's variables first among them,
|
|
77
|
+
* but also every other credential the operator's environment happens to hold.
|
|
78
|
+
*
|
|
79
|
+
* `HOME` stays real: Claude Code finds its login through it, and a session
|
|
80
|
+
* with no tools cannot read anything there.
|
|
81
|
+
*/
|
|
82
|
+
export declare function venueEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
83
|
+
/** A fresh empty directory for one venue session, and its removal. */
|
|
84
|
+
export declare function venueWorkdir(): {
|
|
85
|
+
dir: string;
|
|
86
|
+
remove: () => void;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The `/ws/webhooks` contract for venue sessions. Mirrors `VENUE_SESSION_*` in
|
|
90
|
+
* the server's `shared/agents/venue.py`: the feature `auth_ok` lists, the frame
|
|
91
|
+
* this harness sends to open one, and the frame it gets back.
|
|
92
|
+
*/
|
|
93
|
+
export declare const VENUE_SESSION_FEATURE = "venue_session";
|
|
94
|
+
export declare const VENUE_SESSION_OPEN_FRAME = "venue_session_open";
|
|
95
|
+
export declare const VENUE_SESSION_FRAME = "venue_session";
|
|
96
|
+
/** An opened venue session: the run as the agent will see it, token attached. */
|
|
97
|
+
export type VenueSessionResult = {
|
|
98
|
+
ok: true;
|
|
99
|
+
run: AgentRunRequest;
|
|
100
|
+
} | {
|
|
101
|
+
ok: false;
|
|
102
|
+
error: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The run as its venue session answered it, with the thread and the token.
|
|
106
|
+
*
|
|
107
|
+
* The session is opened over this harness's own socket and nowhere else — the
|
|
108
|
+
* server keeps no HTTP route to it, so a process that merely holds this
|
|
109
|
+
* machine's key (an ordinary agent session) cannot ask for one the way the
|
|
110
|
+
* harness does. The token is attached to the returned run and goes nowhere
|
|
111
|
+
* else: no error here repeats the answer.
|
|
112
|
+
*/
|
|
113
|
+
export declare function venueRunFromSession(run: AgentRunRequest, data: Record<string, unknown>, answer: Record<string, unknown>, clock?: VenueClock): VenueSessionResult;
|
|
114
|
+
/**
|
|
115
|
+
* When a venue session was asked for, and the time now, on both clocks. Each
|
|
116
|
+
* defaults to the real clock; tests pin them.
|
|
117
|
+
*/
|
|
118
|
+
export interface VenueClock {
|
|
119
|
+
openedAtMono?: number;
|
|
120
|
+
openedAtWall?: number;
|
|
121
|
+
nowMono?: number;
|
|
122
|
+
wallNow?: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* How long before its deadline a venue grant already counts as over: room to
|
|
126
|
+
* close the run with its token while the token still works.
|
|
127
|
+
*/
|
|
128
|
+
export declare const VENUE_EXPIRY_MARGIN_MS = 30000;
|
|
129
|
+
/**
|
|
130
|
+
* How often a running venue run's grant is re-checked: the longest a run can
|
|
131
|
+
* outlive its grant after the machine wakes from a sleep.
|
|
132
|
+
*/
|
|
133
|
+
export declare const VENUE_GRANT_CHECK_MS = 15000;
|
|
134
|
+
/** The completion error of a venue run whose grant ran out before it started. */
|
|
135
|
+
export declare const VENUE_GRANT_EXPIRED_ERROR = "connected-team grant expired before the run started";
|
|
136
|
+
/** The completion error of a venue run stopped because its grant was ending. */
|
|
137
|
+
export declare const VENUE_GRANT_ENDED_ERROR = "connected-team grant expired while the run was working";
|
|
138
|
+
/**
|
|
139
|
+
* How much of a venue run's grant is left, in milliseconds — negative once it
|
|
140
|
+
* is over, and `-Infinity` for a run that carries no grant.
|
|
141
|
+
*
|
|
142
|
+
* The lifetime is counted on both clocks and the shorter one wins: the
|
|
143
|
+
* monotonic clock cannot be wound back, and the wall clock keeps counting
|
|
144
|
+
* while the machine sleeps (Linux's monotonic clock does not), so neither a
|
|
145
|
+
* changed clock nor a suspend outlasts the token. Both count elapsed time from
|
|
146
|
+
* the same moment, so a skewed wall clock still cancels out.
|
|
147
|
+
*/
|
|
148
|
+
export declare function venueGrantRemainingMs(run: AgentRunRequest, nowMono?: number, wallNow?: number): number;
|
|
149
|
+
/**
|
|
150
|
+
* Whether a venue run's grant is still good — checked when its session opens
|
|
151
|
+
* and again right before the agent is started, so a run that waited in the
|
|
152
|
+
* queue past its grant never starts.
|
|
153
|
+
*/
|
|
154
|
+
export declare function venueGrantLive(run: AgentRunRequest, nowMono?: number, wallNow?: number): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* What a venue run reports about itself while it works and when it fails.
|
|
157
|
+
*
|
|
158
|
+
* The run's progress line and failure detail land on its row in this agent's
|
|
159
|
+
* home workspace. A venue run's would be the agent's words about the
|
|
160
|
+
* connected team's thread — or the thread itself, quoted — so it reports a
|
|
161
|
+
* fixed line instead.
|
|
162
|
+
*/
|
|
163
|
+
export declare const VENUE_PROGRESS = "answering in a connected team's thread";
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs that answer in a connected team's thread — a *venue*.
|
|
3
|
+
*
|
|
4
|
+
* Connected Teams let two ProhostAI workspaces talk in each other's team chat,
|
|
5
|
+
* so this agent can be addressed in a thread that belongs to the OTHER
|
|
6
|
+
* workspace. That thread's text is someone else's prompt, and the harness's own
|
|
7
|
+
* credential can write anywhere in this agent's home workspace. So a venue run
|
|
8
|
+
* is answered by a session that holds none of it:
|
|
9
|
+
*
|
|
10
|
+
* * **Its own credential.** The event carries no thread text at all. The
|
|
11
|
+
* harness opens the run's *venue session* over its own `/ws/webhooks` socket
|
|
12
|
+
* (there is no HTTP route to it) and gets back a token minted for this one
|
|
13
|
+
* run together with the thread. The token reads and posts in that one thread and heartbeats,
|
|
14
|
+
* streams and completes that one run — nothing else — and the server refuses
|
|
15
|
+
* the harness's key for all of it. Every request the run makes carries the
|
|
16
|
+
* token. It lives in memory only: never logged, never written to disk.
|
|
17
|
+
* * **No tools, no configuration.** Claude Code is started from the operator's
|
|
18
|
+
* binary and model only, with `--tools ''` (no built-in tools, so nothing can
|
|
19
|
+
* read the credentials file or the environment), `--safe-mode` and
|
|
20
|
+
* `--setting-sources '' --settings '{}'` (no hooks, plugins, skills,
|
|
21
|
+
* `CLAUDE.md`, permissions or settings from `~/.claude` or any project), an
|
|
22
|
+
* empty strict MCP config (never the ProhostAI server or the operator's extra
|
|
23
|
+
* servers), and `--no-session-persistence` (the connected team's text is not
|
|
24
|
+
* written to a transcript). The agent answers from the thread; the harness
|
|
25
|
+
* posts the reply.
|
|
26
|
+
* * **No home environment.** The child gets an allowlisted environment —
|
|
27
|
+
* what a shell and the Claude login need, nothing of ProhostAI's — and an
|
|
28
|
+
* empty directory of its own, removed afterwards.
|
|
29
|
+
*
|
|
30
|
+
* Only Claude Code can be held to that, and only a release that has every one
|
|
31
|
+
* of those flags, so only such a harness declares
|
|
32
|
+
* {@link VENUE_RUN_TOKEN_CAPABILITY} — and the server sends venue runs only to
|
|
33
|
+
* a runtime that declared it. A venue run that reaches any other harness anyway
|
|
34
|
+
* is refused without being run.
|
|
35
|
+
*/
|
|
36
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { parseRunRequest } from './contract.js';
|
|
40
|
+
import { shellQuote } from './shell.js';
|
|
41
|
+
/**
|
|
42
|
+
* What a harness declares in its `runtime_status` frame when it runs venue
|
|
43
|
+
* sessions as described above. Mirrors `VENUE_RUN_TOKEN_CAPABILITY` in the
|
|
44
|
+
* server's `shared/agents/runtime_status.py`.
|
|
45
|
+
*/
|
|
46
|
+
export const VENUE_RUN_TOKEN_CAPABILITY = 'venue_run_token';
|
|
47
|
+
/** The token prefix the server mints. Anything else is not a run token. */
|
|
48
|
+
export const VENUE_RUN_TOKEN_PREFIX = 'pvr_';
|
|
49
|
+
/** Every flag {@link claudeVenueCommand} uses. A build missing one cannot run a venue session. */
|
|
50
|
+
const VENUE_FLAGS = [
|
|
51
|
+
'--tools',
|
|
52
|
+
'--safe-mode',
|
|
53
|
+
'--setting-sources',
|
|
54
|
+
'--settings',
|
|
55
|
+
'--strict-mcp-config',
|
|
56
|
+
'--mcp-config',
|
|
57
|
+
'--no-session-persistence',
|
|
58
|
+
];
|
|
59
|
+
/** Whether a run answers in a connected team's thread. */
|
|
60
|
+
export function isVenueRun(run) {
|
|
61
|
+
return run.venue !== undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The API options a run's requests go out with: its own token for a venue run
|
|
65
|
+
* whose session is open, the harness's credential for every other.
|
|
66
|
+
*/
|
|
67
|
+
export function apiForRun(api, run) {
|
|
68
|
+
return isVenueRun(run) && run.venue_run_token ? { ...api, apiKey: run.venue_run_token } : api;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether this Claude Code binary has every flag a venue session depends on.
|
|
72
|
+
*
|
|
73
|
+
* Asked of `--help` rather than inferred from a version number: the flags are
|
|
74
|
+
* what matters, and a build without one of them would reject the command
|
|
75
|
+
* outright — or, worse, a future one might accept and ignore it. Windows is
|
|
76
|
+
* left out because its shell quoting cannot be relied on to hand `--tools` an
|
|
77
|
+
* empty value.
|
|
78
|
+
*/
|
|
79
|
+
export function claudeSupportsVenueSessions(helpText, platform = process.platform) {
|
|
80
|
+
if (platform === 'win32')
|
|
81
|
+
return false;
|
|
82
|
+
return VENUE_FLAGS.every((flag) => new RegExp(`(^|\\s)${flag.replace(/-/g, '\\-')}(\\s|,|$)`, 'm').test(helpText));
|
|
83
|
+
}
|
|
84
|
+
/** An empty MCP config, inline: there is no file another run could rewrite under us. */
|
|
85
|
+
const EMPTY_MCP_CONFIG = '{"mcpServers":{}}';
|
|
86
|
+
/**
|
|
87
|
+
* The Claude Code command a venue run is answered with.
|
|
88
|
+
*
|
|
89
|
+
* Built from the operator's binary and model only — never the rest of their
|
|
90
|
+
* `--exec` string, whose flags (`--add-dir`, `--mcp-config`, `--allowedTools`,
|
|
91
|
+
* `--dangerously-skip-permissions`) are exactly what a venue session must not
|
|
92
|
+
* get.
|
|
93
|
+
*/
|
|
94
|
+
export function claudeVenueCommand(exec, model, platform = process.platform) {
|
|
95
|
+
const quote = (value) => shellQuote(value, platform);
|
|
96
|
+
const binary = exec.trim().split(/\s+/)[0] ?? 'claude';
|
|
97
|
+
const parts = [binary, '-p'];
|
|
98
|
+
if (model)
|
|
99
|
+
parts.push('--model', quote(model));
|
|
100
|
+
parts.push('--output-format', 'stream-json', '--verbose', '--no-session-persistence', '--safe-mode', '--setting-sources', quote(''), '--settings', quote('{}'), '--tools', quote(''),
|
|
101
|
+
// Last: `--mcp-config` is variadic and would swallow anything after it
|
|
102
|
+
// that is not itself a flag.
|
|
103
|
+
'--strict-mcp-config', '--mcp-config', quote(EMPTY_MCP_CONFIG));
|
|
104
|
+
return parts.join(' ');
|
|
105
|
+
}
|
|
106
|
+
/** Variables a venue session keeps by name: what a shell and the TLS stack need. */
|
|
107
|
+
const VENUE_ENV_NAMES = new Set([
|
|
108
|
+
'PATH',
|
|
109
|
+
'HOME',
|
|
110
|
+
'USER',
|
|
111
|
+
'LOGNAME',
|
|
112
|
+
'SHELL',
|
|
113
|
+
'LANG',
|
|
114
|
+
'TERM',
|
|
115
|
+
'TMPDIR',
|
|
116
|
+
'TZ',
|
|
117
|
+
'NODE_EXTRA_CA_CERTS',
|
|
118
|
+
'SSL_CERT_FILE',
|
|
119
|
+
'SSL_CERT_DIR',
|
|
120
|
+
'HTTPS_PROXY',
|
|
121
|
+
'HTTP_PROXY',
|
|
122
|
+
'NO_PROXY',
|
|
123
|
+
'https_proxy',
|
|
124
|
+
'http_proxy',
|
|
125
|
+
'no_proxy',
|
|
126
|
+
]);
|
|
127
|
+
/**
|
|
128
|
+
* The environment a venue session's child gets, built up rather than pared
|
|
129
|
+
* down: the shell basics above, the locale, and Claude Code's own login and
|
|
130
|
+
* configuration (`CLAUDE_*`, `ANTHROPIC_*` — the brain cannot start without
|
|
131
|
+
* them). Everything else is dropped — ProhostAI's variables first among them,
|
|
132
|
+
* but also every other credential the operator's environment happens to hold.
|
|
133
|
+
*
|
|
134
|
+
* `HOME` stays real: Claude Code finds its login through it, and a session
|
|
135
|
+
* with no tools cannot read anything there.
|
|
136
|
+
*/
|
|
137
|
+
export function venueEnv(base) {
|
|
138
|
+
const env = {};
|
|
139
|
+
for (const [key, value] of Object.entries(base)) {
|
|
140
|
+
if (value === undefined || /PROHOST/i.test(key))
|
|
141
|
+
continue;
|
|
142
|
+
if (VENUE_ENV_NAMES.has(key) || key.startsWith('LC_') || /^(CLAUDE|ANTHROPIC)_/.test(key))
|
|
143
|
+
env[key] = value;
|
|
144
|
+
}
|
|
145
|
+
return env;
|
|
146
|
+
}
|
|
147
|
+
/** A fresh empty directory for one venue session, and its removal. */
|
|
148
|
+
export function venueWorkdir() {
|
|
149
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'prohost-venue-'));
|
|
150
|
+
return {
|
|
151
|
+
dir,
|
|
152
|
+
remove: () => {
|
|
153
|
+
try {
|
|
154
|
+
rmSync(dir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Best effort: an empty temp directory left behind costs nothing.
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The `/ws/webhooks` contract for venue sessions. Mirrors `VENUE_SESSION_*` in
|
|
164
|
+
* the server's `shared/agents/venue.py`: the feature `auth_ok` lists, the frame
|
|
165
|
+
* this harness sends to open one, and the frame it gets back.
|
|
166
|
+
*/
|
|
167
|
+
export const VENUE_SESSION_FEATURE = 'venue_session';
|
|
168
|
+
export const VENUE_SESSION_OPEN_FRAME = 'venue_session_open';
|
|
169
|
+
export const VENUE_SESSION_FRAME = 'venue_session';
|
|
170
|
+
/**
|
|
171
|
+
* The run as its venue session answered it, with the thread and the token.
|
|
172
|
+
*
|
|
173
|
+
* The session is opened over this harness's own socket and nowhere else — the
|
|
174
|
+
* server keeps no HTTP route to it, so a process that merely holds this
|
|
175
|
+
* machine's key (an ordinary agent session) cannot ask for one the way the
|
|
176
|
+
* harness does. The token is attached to the returned run and goes nowhere
|
|
177
|
+
* else: no error here repeats the answer.
|
|
178
|
+
*/
|
|
179
|
+
export function venueRunFromSession(run, data, answer, clock = {}) {
|
|
180
|
+
if (answer.ok !== true)
|
|
181
|
+
return { ok: false, error: 'the server refused the venue session' };
|
|
182
|
+
const token = typeof answer.token === 'string' ? answer.token : '';
|
|
183
|
+
if (!token.startsWith(VENUE_RUN_TOKEN_PREFIX))
|
|
184
|
+
return { ok: false, error: 'the venue session carried no run token' };
|
|
185
|
+
const trigger = answer.trigger && typeof answer.trigger === 'object' ? answer.trigger : {};
|
|
186
|
+
const opened = parseRunRequest({
|
|
187
|
+
...data,
|
|
188
|
+
trigger: { ...data.trigger, ...trigger },
|
|
189
|
+
recent_messages: Array.isArray(answer.recent_messages) ? answer.recent_messages : [],
|
|
190
|
+
});
|
|
191
|
+
if (opened === null || opened.run_id !== run.run_id) {
|
|
192
|
+
return { ok: false, error: 'the venue session did not match the run' };
|
|
193
|
+
}
|
|
194
|
+
const lifetime = sessionLifetimeMs(answer, clock.wallNow);
|
|
195
|
+
if (lifetime === undefined)
|
|
196
|
+
return { ok: false, error: 'the venue session carried no expiry' };
|
|
197
|
+
const withGrant = {
|
|
198
|
+
...opened,
|
|
199
|
+
venue_run_token: token,
|
|
200
|
+
venue_deadline_ms: (clock.openedAtMono ?? performance.now()) + lifetime,
|
|
201
|
+
venue_wall_deadline_ms: (clock.openedAtWall ?? Date.now()) + lifetime,
|
|
202
|
+
};
|
|
203
|
+
if (!venueGrantLive(withGrant, clock.nowMono, clock.wallNow)) {
|
|
204
|
+
return { ok: false, error: 'the venue session had already expired' };
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, run: withGrant };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* How long the grant lasts from when the session was asked for, or
|
|
210
|
+
* `undefined` for an answer that states none.
|
|
211
|
+
*
|
|
212
|
+
* The server's remaining lifetime (`expires_in_seconds`) is counted from when
|
|
213
|
+
* the session was asked for — never later than the server started counting —
|
|
214
|
+
* so neither a skewed wall clock nor the round trip can lengthen the grant.
|
|
215
|
+
* `expires_at` is the fallback for a server that sends only that, and the one
|
|
216
|
+
* place the wall clock's reading is trusted.
|
|
217
|
+
*/
|
|
218
|
+
function sessionLifetimeMs(answer, wallNow = Date.now()) {
|
|
219
|
+
const remaining = answer.expires_in_seconds;
|
|
220
|
+
if (typeof remaining === 'number' && Number.isFinite(remaining))
|
|
221
|
+
return Math.max(0, remaining) * 1000;
|
|
222
|
+
const expiresAt = typeof answer.expires_at === 'string' ? Date.parse(answer.expires_at) : Number.NaN;
|
|
223
|
+
if (!Number.isFinite(expiresAt))
|
|
224
|
+
return undefined;
|
|
225
|
+
return expiresAt - wallNow;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* How long before its deadline a venue grant already counts as over: room to
|
|
229
|
+
* close the run with its token while the token still works.
|
|
230
|
+
*/
|
|
231
|
+
export const VENUE_EXPIRY_MARGIN_MS = 30_000;
|
|
232
|
+
/**
|
|
233
|
+
* How often a running venue run's grant is re-checked: the longest a run can
|
|
234
|
+
* outlive its grant after the machine wakes from a sleep.
|
|
235
|
+
*/
|
|
236
|
+
export const VENUE_GRANT_CHECK_MS = 15_000;
|
|
237
|
+
/** The completion error of a venue run whose grant ran out before it started. */
|
|
238
|
+
export const VENUE_GRANT_EXPIRED_ERROR = 'connected-team grant expired before the run started';
|
|
239
|
+
/** The completion error of a venue run stopped because its grant was ending. */
|
|
240
|
+
export const VENUE_GRANT_ENDED_ERROR = 'connected-team grant expired while the run was working';
|
|
241
|
+
/**
|
|
242
|
+
* How much of a venue run's grant is left, in milliseconds — negative once it
|
|
243
|
+
* is over, and `-Infinity` for a run that carries no grant.
|
|
244
|
+
*
|
|
245
|
+
* The lifetime is counted on both clocks and the shorter one wins: the
|
|
246
|
+
* monotonic clock cannot be wound back, and the wall clock keeps counting
|
|
247
|
+
* while the machine sleeps (Linux's monotonic clock does not), so neither a
|
|
248
|
+
* changed clock nor a suspend outlasts the token. Both count elapsed time from
|
|
249
|
+
* the same moment, so a skewed wall clock still cancels out.
|
|
250
|
+
*/
|
|
251
|
+
export function venueGrantRemainingMs(run, nowMono = performance.now(), wallNow = Date.now()) {
|
|
252
|
+
if (run.venue_deadline_ms === undefined || run.venue_wall_deadline_ms === undefined)
|
|
253
|
+
return -Infinity;
|
|
254
|
+
return Math.min(run.venue_deadline_ms - nowMono, run.venue_wall_deadline_ms - wallNow);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Whether a venue run's grant is still good — checked when its session opens
|
|
258
|
+
* and again right before the agent is started, so a run that waited in the
|
|
259
|
+
* queue past its grant never starts.
|
|
260
|
+
*/
|
|
261
|
+
export function venueGrantLive(run, nowMono = performance.now(), wallNow = Date.now()) {
|
|
262
|
+
return venueGrantRemainingMs(run, nowMono, wallNow) > VENUE_EXPIRY_MARGIN_MS;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* What a venue run reports about itself while it works and when it fails.
|
|
266
|
+
*
|
|
267
|
+
* The run's progress line and failure detail land on its row in this agent's
|
|
268
|
+
* home workspace. A venue run's would be the agent's words about the
|
|
269
|
+
* connected team's thread — or the thread itself, quoted — so it reports a
|
|
270
|
+
* fixed line instead.
|
|
271
|
+
*/
|
|
272
|
+
export const VENUE_PROGRESS = "answering in a connected team's thread";
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.
|
|
10
|
+
export declare const CLI_VERSION = "0.13.2";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.13.2";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.
|
|
10
|
+
export const CLI_VERSION = '0.13.2';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prohost/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Run your own AI agent as a ProhostAI teammate, and stream your account's webhooks to your laptop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://prohost.ai",
|
|
8
8
|
"bugs": {
|
|
9
9
|
"email": "support@prohost.ai"
|
|
10
10
|
},
|