@phnx-labs/agents-cli 1.22.37 → 1.22.39
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 +50 -0
- package/README.md +8 -8
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/artifacts-setup.d.ts +53 -0
- package/dist/commands/{setup-share.js → artifacts-setup.js} +59 -13
- package/dist/commands/artifacts.d.ts +18 -0
- package/dist/commands/artifacts.js +58 -0
- package/dist/commands/browser.js +2 -0
- package/dist/commands/config.js +31 -1
- package/dist/commands/exec.js +2 -2
- package/dist/commands/models.js +67 -0
- package/dist/commands/setup.js +5 -5
- package/dist/commands/share.d.ts +20 -7
- package/dist/commands/share.js +74 -75
- package/dist/commands/ssh.js +156 -8
- package/dist/lib/browser/hygiene.d.ts +90 -0
- package/dist/lib/browser/hygiene.js +146 -0
- package/dist/lib/browser/ipc.js +12 -0
- package/dist/lib/browser/service.d.ts +75 -1
- package/dist/lib/browser/service.js +201 -11
- package/dist/lib/browser/types.d.ts +44 -1
- package/dist/lib/config-keys.d.ts +11 -3
- package/dist/lib/config-keys.js +22 -3
- package/dist/lib/config-machine-keys.js +1 -0
- package/dist/lib/device-config.d.ts +34 -0
- package/dist/lib/device-config.js +96 -0
- package/dist/lib/devices/pool.d.ts +56 -0
- package/dist/lib/devices/pool.js +85 -0
- package/dist/lib/exec.d.ts +4 -1
- package/dist/lib/exec.js +8 -2
- package/dist/lib/git.d.ts +1 -1
- package/dist/lib/git.js +1 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +36 -0
- package/dist/lib/migrate.js +107 -0
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/share/analytics.js +1 -1
- package/dist/lib/share/capture.d.ts +1 -1
- package/dist/lib/share/capture.js +3 -3
- package/dist/lib/share/config.d.ts +2 -2
- package/dist/lib/share/config.js +5 -5
- package/dist/lib/share/delete.js +3 -3
- package/dist/lib/share/provision.js +3 -3
- package/dist/lib/share/publish.d.ts +1 -1
- package/dist/lib/share/publish.js +3 -3
- package/dist/lib/share/worker-template.d.ts +12 -1
- package/dist/lib/share/worker-template.js +13 -2
- package/dist/lib/smart-launch.d.ts +33 -3
- package/dist/lib/smart-launch.js +61 -6
- package/dist/lib/startup/command-registry.d.ts +10 -2
- package/dist/lib/startup/command-registry.js +16 -7
- package/dist/lib/tmux/orphan-reap.d.ts +15 -19
- package/dist/lib/tmux/orphan-reap.js +15 -21
- package/dist/lib/tmux/session.js +4 -3
- package/dist/lib/triggers/handlers.js +10 -0
- package/dist/lib/triggers/webhook.js +10 -0
- package/dist/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/commands/set.d.ts +0 -15
- package/dist/commands/set.js +0 -79
- package/dist/commands/setup-share.d.ts +0 -17
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -58,6 +58,42 @@ export declare function foldBrowserSessionsIntoProfiles(browserDir?: string): vo
|
|
|
58
58
|
* can drive a fixture tree without touching the user's ~/.agents.
|
|
59
59
|
*/
|
|
60
60
|
export declare function repairSelfReferentialBinShims(versionsRoot?: string, shimsDir?: string, historyDir?: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Move the auto-detected `default` browser profile OUT of the committed central
|
|
63
|
+
* agents.yaml and into this machine's per-device file.
|
|
64
|
+
*
|
|
65
|
+
* `browser` is a CENTRAL key because named profiles a user creates are real fleet
|
|
66
|
+
* config, but the ONE `default` entry inside it is machine-local: its `binary` is
|
|
67
|
+
* an OS-specific path and its endpoint is a locally-chosen free port.
|
|
68
|
+
* `createProfile`/`updateProfile` already route that entry to `deviceBrowser`
|
|
69
|
+
* (`isMachineLocalProfile`, browser/profiles.ts) — but nothing ever removed the
|
|
70
|
+
* copy older versions had already written into the shared file, and
|
|
71
|
+
* `serializeCentral` cannot: it deletes whole KEYS that are device-scoped, and
|
|
72
|
+
* `browser` is not one.
|
|
73
|
+
*
|
|
74
|
+
* So the entry sat in the committed file and every box rewrote it with its own
|
|
75
|
+
* browser. Measured 2026-08-13, all three boxes on 1.22.38 (which HAS the writer
|
|
76
|
+
* fix) with an empty `deviceBrowser` and a machine-specific `default` in central:
|
|
77
|
+
*
|
|
78
|
+
* zion browser: chrome binary: /Applications/Google Chrome.app/...
|
|
79
|
+
* yosemite-s1 browser: brave binary: /opt/brave.com/brave/brave
|
|
80
|
+
* mark-1 (same shape)
|
|
81
|
+
*
|
|
82
|
+
* `agents repos pull user` refuses when an incoming change touches a locally
|
|
83
|
+
* modified path, so agents.yaml being permanently dirty wedged the fleet config
|
|
84
|
+
* sync outright — those boxes sat 5, 8 and 79 commits behind. (RUSH-2161)
|
|
85
|
+
*
|
|
86
|
+
* Idempotent: no-op once central carries no `default` entry. The device file is
|
|
87
|
+
* written FIRST so a crash between the two writes can never lose the profile,
|
|
88
|
+
* and an entry already in the device file wins (this machine's live value is
|
|
89
|
+
* newer than the stale central copy by construction).
|
|
90
|
+
*
|
|
91
|
+
* Central is edited through a `yaml.Document` rather than re-stringified, so the
|
|
92
|
+
* hand-written comments in the committed agents.yaml survive — a plain
|
|
93
|
+
* `yaml.stringify` would drop every one of them and rewrite the whole file,
|
|
94
|
+
* which is the same churn this migration exists to stop (see `serializeCentral`).
|
|
95
|
+
*/
|
|
96
|
+
export declare function migrateMachineLocalBrowserProfileOutOfCentral(userDir?: string, machine?: string): void;
|
|
61
97
|
/**
|
|
62
98
|
* Rename the legacy `extras-extras/` plugin-marketplace dir to `agents-extras/`
|
|
63
99
|
* inside every installed agent version-home, and rewrite cross-references in
|
package/dist/lib/migrate.js
CHANGED
|
@@ -21,6 +21,10 @@ import { setConfigValue } from './device-config.js';
|
|
|
21
21
|
import { enabledRoutineNames, replaceEnabledRoutines } from './routine-activation.js';
|
|
22
22
|
import { evaluateActivationReadiness } from './routine-readiness.js';
|
|
23
23
|
import { migrateDeviceConfigToCentral } from './devices/config-migration.js';
|
|
24
|
+
// Two constants only, never the read/write API — migrations still operate on raw
|
|
25
|
+
// YAML so they never take the meta lock or prime the meta cache mid-migration.
|
|
26
|
+
import { DEFAULT_BROWSER_PROFILE_NAME } from './browser/profiles.js';
|
|
27
|
+
import { META_HEADER as DEVICE_META_HEADER } from './state.js';
|
|
24
28
|
const HOME = process.env.HOME ?? os.homedir();
|
|
25
29
|
const USER_DIR = path.join(HOME, '.agents');
|
|
26
30
|
/** Canonical system-repo location (post-fold). */
|
|
@@ -1688,6 +1692,103 @@ function migrateSplitDeviceLocalMeta() {
|
|
|
1688
1692
|
console.error('Split agents.yaml: agents: -> devices/, versions: -> .history/version-resources.json');
|
|
1689
1693
|
}
|
|
1690
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Move the auto-detected `default` browser profile OUT of the committed central
|
|
1697
|
+
* agents.yaml and into this machine's per-device file.
|
|
1698
|
+
*
|
|
1699
|
+
* `browser` is a CENTRAL key because named profiles a user creates are real fleet
|
|
1700
|
+
* config, but the ONE `default` entry inside it is machine-local: its `binary` is
|
|
1701
|
+
* an OS-specific path and its endpoint is a locally-chosen free port.
|
|
1702
|
+
* `createProfile`/`updateProfile` already route that entry to `deviceBrowser`
|
|
1703
|
+
* (`isMachineLocalProfile`, browser/profiles.ts) — but nothing ever removed the
|
|
1704
|
+
* copy older versions had already written into the shared file, and
|
|
1705
|
+
* `serializeCentral` cannot: it deletes whole KEYS that are device-scoped, and
|
|
1706
|
+
* `browser` is not one.
|
|
1707
|
+
*
|
|
1708
|
+
* So the entry sat in the committed file and every box rewrote it with its own
|
|
1709
|
+
* browser. Measured 2026-08-13, all three boxes on 1.22.38 (which HAS the writer
|
|
1710
|
+
* fix) with an empty `deviceBrowser` and a machine-specific `default` in central:
|
|
1711
|
+
*
|
|
1712
|
+
* zion browser: chrome binary: /Applications/Google Chrome.app/...
|
|
1713
|
+
* yosemite-s1 browser: brave binary: /opt/brave.com/brave/brave
|
|
1714
|
+
* mark-1 (same shape)
|
|
1715
|
+
*
|
|
1716
|
+
* `agents repos pull user` refuses when an incoming change touches a locally
|
|
1717
|
+
* modified path, so agents.yaml being permanently dirty wedged the fleet config
|
|
1718
|
+
* sync outright — those boxes sat 5, 8 and 79 commits behind. (RUSH-2161)
|
|
1719
|
+
*
|
|
1720
|
+
* Idempotent: no-op once central carries no `default` entry. The device file is
|
|
1721
|
+
* written FIRST so a crash between the two writes can never lose the profile,
|
|
1722
|
+
* and an entry already in the device file wins (this machine's live value is
|
|
1723
|
+
* newer than the stale central copy by construction).
|
|
1724
|
+
*
|
|
1725
|
+
* Central is edited through a `yaml.Document` rather than re-stringified, so the
|
|
1726
|
+
* hand-written comments in the committed agents.yaml survive — a plain
|
|
1727
|
+
* `yaml.stringify` would drop every one of them and rewrite the whole file,
|
|
1728
|
+
* which is the same churn this migration exists to stop (see `serializeCentral`).
|
|
1729
|
+
*/
|
|
1730
|
+
export function migrateMachineLocalBrowserProfileOutOfCentral(userDir = USER_DIR, machine = machineId()) {
|
|
1731
|
+
const metaFile = path.join(userDir, 'agents.yaml');
|
|
1732
|
+
if (!fs.existsSync(metaFile))
|
|
1733
|
+
return;
|
|
1734
|
+
let doc;
|
|
1735
|
+
try {
|
|
1736
|
+
doc = yaml.parseDocument(fs.readFileSync(metaFile, 'utf-8'));
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (doc.errors.length > 0)
|
|
1742
|
+
return;
|
|
1743
|
+
const central = doc.toJSON() ?? {};
|
|
1744
|
+
const browser = central.browser;
|
|
1745
|
+
if (!browser || typeof browser !== 'object' || Array.isArray(browser))
|
|
1746
|
+
return;
|
|
1747
|
+
const entry = browser[DEFAULT_BROWSER_PROFILE_NAME];
|
|
1748
|
+
if (entry === undefined)
|
|
1749
|
+
return;
|
|
1750
|
+
// Device file first — a crash before central is rewritten leaves a harmless
|
|
1751
|
+
// duplicate, while the reverse order would drop the profile entirely.
|
|
1752
|
+
const devicePath = path.join(userDir, 'devices', machine, 'agents.yaml');
|
|
1753
|
+
let deviceDoc = {};
|
|
1754
|
+
try {
|
|
1755
|
+
deviceDoc = yaml.parse(fs.readFileSync(devicePath, 'utf-8')) || {};
|
|
1756
|
+
}
|
|
1757
|
+
catch { /* absent — first write */ }
|
|
1758
|
+
const deviceBrowser = (deviceDoc.browser && typeof deviceDoc.browser === 'object' && !Array.isArray(deviceDoc.browser))
|
|
1759
|
+
? deviceDoc.browser
|
|
1760
|
+
: {};
|
|
1761
|
+
if (deviceBrowser[DEFAULT_BROWSER_PROFILE_NAME] === undefined) {
|
|
1762
|
+
deviceBrowser[DEFAULT_BROWSER_PROFILE_NAME] = entry;
|
|
1763
|
+
deviceDoc.browser = deviceBrowser;
|
|
1764
|
+
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
1765
|
+
atomicWriteFileSync(devicePath, DEVICE_META_HEADER + yaml.stringify(deviceDoc));
|
|
1766
|
+
}
|
|
1767
|
+
// Then strip it from the synced file, dropping `browser:` entirely when the
|
|
1768
|
+
// machine-local entry was its only member.
|
|
1769
|
+
doc.deleteIn(['browser', DEFAULT_BROWSER_PROFILE_NAME]);
|
|
1770
|
+
if (Object.keys(browser).length === 1) {
|
|
1771
|
+
const itemsOf = () => (doc.contents?.items) ?? [];
|
|
1772
|
+
const idx = itemsOf().findIndex((pair) => pair.key?.value === 'browser');
|
|
1773
|
+
const orphaned = idx >= 0 ? itemsOf()[idx]?.key?.commentBefore ?? undefined : undefined;
|
|
1774
|
+
doc.delete('browser');
|
|
1775
|
+
if (orphaned) {
|
|
1776
|
+
// Deleting shifted the following pair down into `idx`.
|
|
1777
|
+
const next = itemsOf()[idx]?.key;
|
|
1778
|
+
if (next)
|
|
1779
|
+
next.commentBefore = next.commentBefore ? `${orphaned}\n${next.commentBefore}` : orphaned;
|
|
1780
|
+
else
|
|
1781
|
+
doc.commentBefore = orphaned;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
// Everything cleared -> header only, never a bare `{}`. stringifyDoc emits a
|
|
1785
|
+
// FLOW empty map for an empty root, and a later parseDocument inherits that
|
|
1786
|
+
// flow and renders the whole rewritten file inline. serializeCentral guards
|
|
1787
|
+
// the identical case (state.ts, `isEmpty ? META_HEADER : stringifyDoc(doc)`).
|
|
1788
|
+
const remaining = Object.keys(doc.toJSON() ?? {}).length;
|
|
1789
|
+
atomicWriteFileSync(metaFile, remaining === 0 ? DEVICE_META_HEADER : stringifyDoc(doc));
|
|
1790
|
+
console.error(`Migrated agents.yaml: browser '${DEFAULT_BROWSER_PROFILE_NAME}' profile -> devices/${machine}/agents.yaml`);
|
|
1791
|
+
}
|
|
1691
1792
|
/**
|
|
1692
1793
|
* Rename the legacy `extras-extras/` plugin-marketplace dir to `agents-extras/`
|
|
1693
1794
|
* inside every installed agent version-home, and rewrite cross-references in
|
|
@@ -2299,6 +2400,12 @@ export async function runMigration() {
|
|
|
2299
2400
|
// agents.yaml. After migrateVersionResourcesToPatterns so versions: is already
|
|
2300
2401
|
// in pattern form when it moves to the history file.
|
|
2301
2402
|
migrateSplitDeviceLocalMeta();
|
|
2403
|
+
// Same split, one level deeper: `browser` stays central (named profiles are
|
|
2404
|
+
// fleet config) but its auto-detected `default` entry is machine-local and was
|
|
2405
|
+
// left behind in the synced file, keeping every box dirty. After
|
|
2406
|
+
// migrateSplitDeviceLocalMeta so the device file is already in its canonical
|
|
2407
|
+
// location before this merges an entry into it.
|
|
2408
|
+
migrateMachineLocalBrowserProfileOutOfCentral();
|
|
2302
2409
|
// Fold per-device operator config (device-doc config:/defaultBrowserProfile
|
|
2303
2410
|
// and .history/devices/auto-launch.json) into the central
|
|
2304
2411
|
// fleet.devices.<name>.config block. After migrateSplitDeviceLocalMeta so the
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -79,7 +79,9 @@ export interface LinearJobTrigger {
|
|
|
79
79
|
teamKey?: string;
|
|
80
80
|
/** Required issue label name. */
|
|
81
81
|
label?: string;
|
|
82
|
-
/**
|
|
82
|
+
/** Fire on a transition INTO this Linear state (e.g. `Plan`): the current state
|
|
83
|
+
* must match AND the delivery's `updatedFrom` must record a state change, so a
|
|
84
|
+
* later edit that leaves the issue in this state does not re-fire (RUSH-2539). */
|
|
83
85
|
stateTo?: string;
|
|
84
86
|
/** Previous Linear state name that must match (e.g. `Triage`). */
|
|
85
87
|
stateFrom?: string;
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Cloudflare Web Analytics injection for `agents share`.
|
|
1
|
+
// Cloudflare Web Analytics injection for `agents artifacts share`.
|
|
2
2
|
//
|
|
3
3
|
// The beacon is cookieless and privacy-first, which matters because a lot of shared
|
|
4
4
|
// content (games, kid-facing pages) should avoid GA4-style tracking. The token is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Render an HTML file to a 1200×630 PNG — the Open Graph cover for a shared plan.
|
|
3
3
|
*
|
|
4
|
-
* When `agents share plan.html` runs, we screenshot the plan's own hero and use it
|
|
4
|
+
* When `agents artifacts share plan.html` runs, we screenshot the plan's own hero and use it
|
|
5
5
|
* as the `og:image`, so the link unfurls into a card in Slack / iMessage / Twitter /
|
|
6
6
|
* Discord. No AI, no central render service: it's a headless screenshot on the
|
|
7
7
|
* publisher's machine, so it works identically for us and for any user, and costs
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Render an HTML file to a 1200×630 PNG — the Open Graph cover for a shared plan.
|
|
3
3
|
*
|
|
4
|
-
* When `agents share plan.html` runs, we screenshot the plan's own hero and use it
|
|
4
|
+
* When `agents artifacts share plan.html` runs, we screenshot the plan's own hero and use it
|
|
5
5
|
* as the `og:image`, so the link unfurls into a card in Slack / iMessage / Twitter /
|
|
6
6
|
* Discord. No AI, no central render service: it's a headless screenshot on the
|
|
7
7
|
* publisher's machine, so it works identically for us and for any user, and costs
|
|
@@ -112,7 +112,7 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
112
112
|
// A silently-dropped cover reads as "the CLI decided this page needs none",
|
|
113
113
|
// when in fact no headless browser was found. Say so and point at the escape
|
|
114
114
|
// hatch instead of leaving the publish coverless with no explanation.
|
|
115
|
-
process.stderr.write('[agents share] no headless browser found for the OG cover — install Chrome/Chromium ' +
|
|
115
|
+
process.stderr.write('[agents artifacts share] no headless browser found for the OG cover — install Chrome/Chromium ' +
|
|
116
116
|
'or set AGENTS_SHARE_BROWSER=/path/to/chrome. Publishing without a preview image.\n');
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
@@ -159,7 +159,7 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
159
159
|
}
|
|
160
160
|
// Every candidate ran but none yielded a cover — surface the last reason so a
|
|
161
161
|
// missing preview card is diagnosable (timeout, crash, bad binary) rather than silent.
|
|
162
|
-
process.stderr.write(`[agents share] OG cover capture failed (${lastFailure || 'unknown error'}) — ` +
|
|
162
|
+
process.stderr.write(`[agents artifacts share] OG cover capture failed (${lastFailure || 'unknown error'}) — ` +
|
|
163
163
|
'publishing without a preview image. Set AGENTS_SHARE_BROWSER to override.\n');
|
|
164
164
|
return null;
|
|
165
165
|
}
|
|
@@ -22,7 +22,7 @@ export declare const DEFAULT_BUCKET_NAME = "agents-share";
|
|
|
22
22
|
export declare const DEFAULT_SHARE_DOMAIN = "share.agents-cli.sh";
|
|
23
23
|
/** The write token may be injected ephemerally into fleet/cloud agents. */
|
|
24
24
|
export declare function readWriteTokenEnv(env?: NodeJS.ProcessEnv): string | null;
|
|
25
|
-
/** Read the persisted endpoint config, or null if `agents share
|
|
25
|
+
/** Read the persisted endpoint config, or null if `agents artifacts setup` / `agents artifacts share join` never ran. */
|
|
26
26
|
export declare function readShareConfig(): ShareConfig | null;
|
|
27
27
|
/** Persist the endpoint config to `agents.yaml` (syncs across the fleet). */
|
|
28
28
|
export declare function writeShareConfig(cfg: ShareConfig): void;
|
|
@@ -46,7 +46,7 @@ export declare function readWriteToken(): string;
|
|
|
46
46
|
* launch. The read is now always `agentOnly` — it resolves the token only from the
|
|
47
47
|
* injected env or an already-held / no-ACL bundle, and silently returns undefined
|
|
48
48
|
* otherwise (the caller runs without auto-share; the agent can still publish via
|
|
49
|
-
* its own `agents share`). To get zero-friction auto-share with no prompt: unlock
|
|
49
|
+
* its own `agents artifacts share`). To get zero-friction auto-share with no prompt: unlock
|
|
50
50
|
* once (`agents secrets unlock share`) or make it no-ACL (`agents secrets policy
|
|
51
51
|
* share never`). */
|
|
52
52
|
export declare function shareRuntimeEnv(): Record<string, string> | undefined;
|
package/dist/lib/share/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Config + credential glue for `agents share`.
|
|
1
|
+
// Config + credential glue for `agents artifacts share`.
|
|
2
2
|
//
|
|
3
3
|
// - The endpoint config (base URL, account, worker/bucket names) lives in
|
|
4
4
|
// `agents.yaml` under `share:` (Meta.share) so it syncs fleet-wide via
|
|
@@ -23,7 +23,7 @@ export function readWriteTokenEnv(env = process.env) {
|
|
|
23
23
|
const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
|
|
24
24
|
return token ? token : null;
|
|
25
25
|
}
|
|
26
|
-
/** Read the persisted endpoint config, or null if `agents share
|
|
26
|
+
/** Read the persisted endpoint config, or null if `agents artifacts setup` / `agents artifacts share join` never ran. */
|
|
27
27
|
export function readShareConfig() {
|
|
28
28
|
const s = readMeta().share;
|
|
29
29
|
if (!s?.baseUrl || !s.accountId || !s.workerName || !s.bucketName)
|
|
@@ -56,7 +56,7 @@ export function storeWriteToken(token) {
|
|
|
56
56
|
catch {
|
|
57
57
|
bundle = {
|
|
58
58
|
name: SHARE_BUNDLE,
|
|
59
|
-
description: 'agents share — write token for the R2 share endpoint',
|
|
59
|
+
description: 'agents artifacts share — write token for the R2 share endpoint',
|
|
60
60
|
// A NEW share bundle defaults to the `never` tier (no biometry ACL). The R2
|
|
61
61
|
// write token is low-sensitivity automation infra that is auto-read on EVERY
|
|
62
62
|
// `agents run` (shareRuntimeEnv) — a biometry ACL there is what produced the
|
|
@@ -83,7 +83,7 @@ export function readWriteTokenFromBundle() {
|
|
|
83
83
|
const token = env[SHARE_TOKEN_KEY];
|
|
84
84
|
if (!token) {
|
|
85
85
|
throw new Error(`No ${SHARE_TOKEN_KEY} in the '${SHARE_BUNDLE}' secrets bundle. ` +
|
|
86
|
-
`Run 'agents
|
|
86
|
+
`Run 'agents artifacts setup' (to provision your own endpoint) or 'agents artifacts share join' (to use an existing one).`);
|
|
87
87
|
}
|
|
88
88
|
return token;
|
|
89
89
|
}
|
|
@@ -102,7 +102,7 @@ export function readWriteToken() {
|
|
|
102
102
|
* launch. The read is now always `agentOnly` — it resolves the token only from the
|
|
103
103
|
* injected env or an already-held / no-ACL bundle, and silently returns undefined
|
|
104
104
|
* otherwise (the caller runs without auto-share; the agent can still publish via
|
|
105
|
-
* its own `agents share`). To get zero-friction auto-share with no prompt: unlock
|
|
105
|
+
* its own `agents artifacts share`). To get zero-friction auto-share with no prompt: unlock
|
|
106
106
|
* once (`agents secrets unlock share`) or make it no-ACL (`agents secrets policy
|
|
107
107
|
* share never`). */
|
|
108
108
|
export function shareRuntimeEnv() {
|
package/dist/lib/share/delete.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The delete path for `agents share delete` / `agents unshare` — an authed DELETE
|
|
1
|
+
// The delete path for `agents artifacts share delete` / `agents unshare` — an authed DELETE
|
|
2
2
|
// to the Worker, which already implements it (worker-template.ts). Mirrors
|
|
3
3
|
// publish.ts: pure target-resolution logic is exported for tests, the network
|
|
4
4
|
// calls (a status check + a delete) sit behind an injectable DI seam.
|
|
@@ -76,7 +76,7 @@ export async function deleteObject(endpoint, key, opts = {}) {
|
|
|
76
76
|
const existedBefore = before.status !== 404;
|
|
77
77
|
const r = await del(url, { authorization: `Bearer ${endpoint.token}` });
|
|
78
78
|
if (!r.ok) {
|
|
79
|
-
throw new Error(`Delete failed (${r.status}) for ${url}. Check the write token, or that 'agents
|
|
79
|
+
throw new Error(`Delete failed (${r.status}) for ${url}. Check the write token, or that 'agents artifacts setup' completed.`);
|
|
80
80
|
}
|
|
81
81
|
const after = await check(url);
|
|
82
82
|
const verified404 = after.status === 404;
|
|
@@ -88,7 +88,7 @@ export async function deleteObject(endpoint, key, opts = {}) {
|
|
|
88
88
|
export async function deleteShare(target, opts = {}) {
|
|
89
89
|
const cfg = opts.config ?? readShareConfig();
|
|
90
90
|
if (!cfg) {
|
|
91
|
-
throw new Error("Not set up yet. Run 'agents
|
|
91
|
+
throw new Error("Not set up yet. Run 'agents artifacts setup' (provision your own endpoint) or 'agents artifacts share join' (use an existing one).");
|
|
92
92
|
}
|
|
93
93
|
const token = opts.writeToken ?? readWriteToken();
|
|
94
94
|
const resolved = await resolveDeleteTarget(target, { githubUser: opts.githubUser });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Cloudflare provisioning for `agents
|
|
1
|
+
// Cloudflare provisioning for `agents artifacts setup` — plain `fetch` against the CF
|
|
2
2
|
// REST API (the repo has no CF wrapper). Creates the R2 bucket, configures its
|
|
3
3
|
// lifecycle, uploads the Worker (with an R2 binding), sets the WRITE_TOKEN secret,
|
|
4
4
|
// enables the free `*.workers.dev` subdomain, and — when the token owns the zone —
|
|
@@ -134,14 +134,14 @@ export async function updateWorker(apiToken, accountId, workerName, bucketName,
|
|
|
134
134
|
await deployWorker(apiToken, accountId, workerName, script, bucketName, opts);
|
|
135
135
|
// Script upload clears bindings/secrets (see JSDoc above). If re-applying
|
|
136
136
|
// WRITE_TOKEN fails here, the live Worker has no write token — every
|
|
137
|
-
// `agents share` publish/delete 401s until a re-run of `agents share update`
|
|
137
|
+
// `agents artifacts share` publish/delete 401s until a re-run of `agents artifacts share update`
|
|
138
138
|
// completes both steps. Surface that explicitly instead of the raw CF error.
|
|
139
139
|
try {
|
|
140
140
|
await setWorkerSecret(apiToken, accountId, workerName, writeToken, opts);
|
|
141
141
|
}
|
|
142
142
|
catch (e) {
|
|
143
143
|
const detail = e instanceof Error ? e.message : String(e);
|
|
144
|
-
throw new Error(`Worker deployed but the write token failed to re-apply — re-run \`agents share update\` to fix this before publishing/deleting anything. (${detail})`);
|
|
144
|
+
throw new Error(`Worker deployed but the write token failed to re-apply — re-run \`agents artifacts share update\` to fix this before publishing/deleting anything. (${detail})`);
|
|
145
145
|
}
|
|
146
146
|
return { templateHash, skipped: false };
|
|
147
147
|
}
|
|
@@ -19,7 +19,7 @@ export interface PublishOptions {
|
|
|
19
19
|
expire?: string;
|
|
20
20
|
contentType?: string;
|
|
21
21
|
/**
|
|
22
|
-
* Hide this page from the public `/<user>` gallery and `agents share list`
|
|
22
|
+
* Hide this page from the public `/<user>` gallery and `agents artifacts share list`
|
|
23
23
|
* (metadata `visibility=unlisted`). The direct URL is still world-readable —
|
|
24
24
|
* unlisted, not secret (RUSH-2443). Alias of `--private` on the CLI.
|
|
25
25
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The publish path for `agents share <file>` — an authed PUT to the Worker.
|
|
1
|
+
// The publish path for `agents artifacts share <file>` — an authed PUT to the Worker.
|
|
2
2
|
// Pure logic (slug, expiry) is exported for tests; the network call is behind a DI seam.
|
|
3
3
|
//
|
|
4
4
|
// For HTML publishes it also captures a 1200×630 cover (the page's own hero) and
|
|
@@ -255,7 +255,7 @@ export function buildShareKey(username, slugPart) {
|
|
|
255
255
|
export async function publishFile(filePath, opts = {}) {
|
|
256
256
|
const cfg = opts.config ?? readShareConfig();
|
|
257
257
|
if (!cfg) {
|
|
258
|
-
throw new Error("Not set up yet. Run 'agents
|
|
258
|
+
throw new Error("Not set up yet. Run 'agents artifacts setup' (provision your own endpoint) or 'agents artifacts share join' (use an existing one).");
|
|
259
259
|
}
|
|
260
260
|
const token = opts.writeToken ?? readWriteToken();
|
|
261
261
|
const username = await resolveShareUsername(opts);
|
|
@@ -318,7 +318,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
318
318
|
}
|
|
319
319
|
const r = await put(pageUrl, body, authHeaders(opts.contentType ?? guessContentType(filePath)));
|
|
320
320
|
if (!r.ok) {
|
|
321
|
-
throw new Error(`Publish failed (${r.status}) for ${pageUrl}. Check the write token, or that 'agents
|
|
321
|
+
throw new Error(`Publish failed (${r.status}) for ${pageUrl}. Check the write token, or that 'agents artifacts setup' completed.`);
|
|
322
322
|
}
|
|
323
323
|
return {
|
|
324
324
|
url: r.url ?? pageUrl,
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
3
|
+
*
|
|
4
|
+
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
5
|
+
* its root response, and its gallery title, even though the command is now
|
|
6
|
+
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
7
|
+
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
8
|
+
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
9
|
+
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
10
|
+
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
11
|
+
* worth that; change this text only alongside a real Worker behavior change.
|
|
12
|
+
*/
|
|
2
13
|
export declare function renderWorkerScript(): string;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// sweeper; this is the immediate gate.
|
|
10
10
|
// - GET /<username> — public gallery of that user's shares (HTML).
|
|
11
11
|
// - GET /<username>?format=json — public machine-readable listing of that user's
|
|
12
|
-
// ACTIVE shares (`agents share list`). Same single-segment path as the HTML
|
|
12
|
+
// ACTIVE shares (`agents artifacts share list`). Same single-segment path as the HTML
|
|
13
13
|
// gallery and gated on the SAME "does <username>/ hold any object" check, so it
|
|
14
14
|
// only intercepts a genuine namespace — a legacy flat slug with ?format=json
|
|
15
15
|
// still serves its real content, never a fake empty listing.
|
|
@@ -19,7 +19,18 @@
|
|
|
19
19
|
// Emitted as a string (mirrors src/lib/serve/page.ts `renderPage()`), so it compiles
|
|
20
20
|
// into `dist/**` and ships with no package.json#files change. `provision.ts` uploads
|
|
21
21
|
// this verbatim as an ES-module Worker with a BUCKET (R2) binding + a WRITE_TOKEN secret.
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
24
|
+
*
|
|
25
|
+
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
26
|
+
* its root response, and its gallery title, even though the command is now
|
|
27
|
+
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
28
|
+
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
29
|
+
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
30
|
+
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
31
|
+
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
32
|
+
* worth that; change this text only alongside a real Worker behavior change.
|
|
33
|
+
*/
|
|
23
34
|
export function renderWorkerScript() {
|
|
24
35
|
return `// GENERATED by agents-cli agents share setup — do not edit here; edit
|
|
25
36
|
// src/lib/share/worker-template.ts and re-run setup.
|
|
@@ -25,14 +25,32 @@ export declare function affinityWeights(rows: AffinityRow[], alpha?: number): We
|
|
|
25
25
|
* Pure — inject `rng` for tests.
|
|
26
26
|
*/
|
|
27
27
|
export declare function sampleWeighted(candidates: WeightedCandidate[], rng?: () => number): string | null;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Online device names from the local registry (+ local), narrowed to the
|
|
30
|
+
* automatic-placement pool.
|
|
31
|
+
*
|
|
32
|
+
* The pool rule lives in `devices/pool.ts` and is an allowlist once any device
|
|
33
|
+
* is marked `role=worker`: this is the single place both automatic-placement
|
|
34
|
+
* paths (`resolveDeviceAuto`, `resolveDeviceAffinity`) get their candidates, so
|
|
35
|
+
* marking workers moves every `--device auto` at once instead of one surface.
|
|
36
|
+
*
|
|
37
|
+
* Paired cockpits (`role: control` in the device registry) are dropped here,
|
|
38
|
+
* where the registry is already being read — they are control surfaces, not
|
|
39
|
+
* compute. It CAN return an empty list — a fleet where every marked worker is
|
|
40
|
+
* offline, or where this box is the only candidate and is marked `personal`. That is a
|
|
41
|
+
* real answer, and both callers fail loud on it rather than falling back to the
|
|
42
|
+
* local machine (which would be the exact box the operator marked personal to
|
|
43
|
+
* keep agents off).
|
|
44
|
+
*/
|
|
29
45
|
export declare function listOnlineDeviceNames(localName?: string): string[];
|
|
30
46
|
export interface DeviceAffinityOptions {
|
|
31
47
|
sinceDays?: number;
|
|
32
48
|
alpha?: number;
|
|
33
49
|
/**
|
|
34
|
-
* Eligible hosts (normalized). Defaults to
|
|
35
|
-
*
|
|
50
|
+
* Eligible hosts (normalized). Defaults to the automatic-placement pool
|
|
51
|
+
* (online devices + local, narrowed by device roles). An explicitly empty
|
|
52
|
+
* list falls back to local — the caller supplied it; an empty DEFAULT pool
|
|
53
|
+
* throws, because roles emptied it on purpose.
|
|
36
54
|
*/
|
|
37
55
|
eligibleHosts?: string[];
|
|
38
56
|
localMachine?: string;
|
|
@@ -59,6 +77,12 @@ export interface DeviceAutoPlan {
|
|
|
59
77
|
}>;
|
|
60
78
|
pickedDeviceKey: string;
|
|
61
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The error both automatic-placement resolvers raise when device roles leave no
|
|
82
|
+
* candidate at all. Fail loud: the alternative — quietly running on the local
|
|
83
|
+
* machine — puts the agent on the box the operator marked `personal`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function formatEmptyAutoPoolError(): string;
|
|
62
86
|
export declare function formatNoHealthyDeviceError(pool: string[], signals: Map<string, DevicePlacementSignal>, agent?: string): string;
|
|
63
87
|
/**
|
|
64
88
|
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
@@ -74,6 +98,12 @@ export declare function resolveDeviceAuto(agent?: string, opts?: {
|
|
|
74
98
|
}): Promise<DeviceAutoPlan>;
|
|
75
99
|
/**
|
|
76
100
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
101
|
+
*
|
|
102
|
+
* Draws from the same automatic-placement pool as {@link resolveDeviceAuto}, so
|
|
103
|
+
* `agents ssh auto`, the generic `--host auto` passthrough, and `matchHost`'s
|
|
104
|
+
* `auto` sentinel honour device roles too. Throws when roles leave the pool
|
|
105
|
+
* empty — a `null` host here means "run locally", which for a box marked
|
|
106
|
+
* `personal` is the outcome the mark exists to prevent.
|
|
77
107
|
*/
|
|
78
108
|
export declare function resolveDeviceAffinity(opts?: DeviceAffinityOptions): DeviceAffinityPlan;
|
|
79
109
|
/** True when a host flag value means affinity pick. */
|
package/dist/lib/smart-launch.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { queryAffinityRollup } from './session/db.js';
|
|
9
9
|
import { localMachineId } from './session/origin-machine.js';
|
|
10
|
-
import { loadDevicesSync } from './devices/registry.js';
|
|
10
|
+
import { isControlDevice, loadDevicesSync } from './devices/registry.js';
|
|
11
|
+
import { describeAutoPool, filterAutoPool, isAutoPoolMember } from './devices/pool.js';
|
|
11
12
|
import { normalizeHost } from './machine-id.js';
|
|
12
13
|
import { probePoolSignals } from './teams/placement-probe.js';
|
|
13
14
|
import { pickBestDevice } from './teams/scheduler.js';
|
|
@@ -45,7 +46,23 @@ export function sampleWeighted(candidates, rng = Math.random) {
|
|
|
45
46
|
}
|
|
46
47
|
return candidates[candidates.length - 1].key;
|
|
47
48
|
}
|
|
48
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Online device names from the local registry (+ local), narrowed to the
|
|
51
|
+
* automatic-placement pool.
|
|
52
|
+
*
|
|
53
|
+
* The pool rule lives in `devices/pool.ts` and is an allowlist once any device
|
|
54
|
+
* is marked `role=worker`: this is the single place both automatic-placement
|
|
55
|
+
* paths (`resolveDeviceAuto`, `resolveDeviceAffinity`) get their candidates, so
|
|
56
|
+
* marking workers moves every `--device auto` at once instead of one surface.
|
|
57
|
+
*
|
|
58
|
+
* Paired cockpits (`role: control` in the device registry) are dropped here,
|
|
59
|
+
* where the registry is already being read — they are control surfaces, not
|
|
60
|
+
* compute. It CAN return an empty list — a fleet where every marked worker is
|
|
61
|
+
* offline, or where this box is the only candidate and is marked `personal`. That is a
|
|
62
|
+
* real answer, and both callers fail loud on it rather than falling back to the
|
|
63
|
+
* local machine (which would be the exact box the operator marked personal to
|
|
64
|
+
* keep agents off).
|
|
65
|
+
*/
|
|
49
66
|
export function listOnlineDeviceNames(localName = localMachineId()) {
|
|
50
67
|
const names = new Set([normalizeHost(localName)]);
|
|
51
68
|
try {
|
|
@@ -57,13 +74,27 @@ export function listOnlineDeviceNames(localName = localMachineId()) {
|
|
|
57
74
|
// No tailscale snapshot → treat as candidate (registry-only box).
|
|
58
75
|
if (online === false)
|
|
59
76
|
continue;
|
|
77
|
+
// A paired cockpit (iPhone/iPad) is a control surface, not compute — it
|
|
78
|
+
// is never dialed for a session, so it is never a placement candidate.
|
|
79
|
+
if (isControlDevice(d))
|
|
80
|
+
continue;
|
|
60
81
|
names.add(normalizeHost(name));
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
catch {
|
|
64
85
|
/* registry missing — local only */
|
|
65
86
|
}
|
|
66
|
-
return [...names];
|
|
87
|
+
return filterAutoPool([...names]);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The error both automatic-placement resolvers raise when device roles leave no
|
|
91
|
+
* candidate at all. Fail loud: the alternative — quietly running on the local
|
|
92
|
+
* machine — puts the agent on the box the operator marked `personal`.
|
|
93
|
+
*/
|
|
94
|
+
export function formatEmptyAutoPoolError() {
|
|
95
|
+
const marked = describeAutoPool();
|
|
96
|
+
return (`agents: no device is eligible for automatic placement${marked ? ` (${marked})` : ''} — ` +
|
|
97
|
+
'mark one with `agents devices role <name> worker`, or widen the pool with `agents config set auto.pool all`.');
|
|
67
98
|
}
|
|
68
99
|
export function formatNoHealthyDeviceError(pool, signals, agent) {
|
|
69
100
|
const excluded = pool.map((key) => {
|
|
@@ -78,7 +109,11 @@ export function formatNoHealthyDeviceError(pool, signals, agent) {
|
|
|
78
109
|
return `${key} (${reason})`;
|
|
79
110
|
}).join(', ');
|
|
80
111
|
const target = agent ? `can run ${agent}` : "for 'run auto'";
|
|
81
|
-
|
|
112
|
+
// Name the role narrowing when there is one: a fleet where every box but two
|
|
113
|
+
// is filtered out by a worker mark reads as "the fleet is down" without it.
|
|
114
|
+
const marked = describeAutoPool();
|
|
115
|
+
const poolNote = marked ? ` [pool: ${marked}]` : '';
|
|
116
|
+
return `agents: no healthy device ${target}${poolNote} — excluded: ${excluded}; earliest window resets unknown`;
|
|
82
117
|
}
|
|
83
118
|
/**
|
|
84
119
|
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
@@ -90,8 +125,13 @@ export function formatNoHealthyDeviceError(pool, signals, agent) {
|
|
|
90
125
|
export async function resolveDeviceAuto(agent, opts = {}) {
|
|
91
126
|
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
92
127
|
const pool = [...new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost))];
|
|
93
|
-
|
|
128
|
+
// The local machine participates in the same probe as every peer — unless a
|
|
129
|
+
// role excludes it. Adding it unconditionally would put agents back on the box
|
|
130
|
+
// the operator marked `personal` precisely to keep them off it.
|
|
131
|
+
if (!pool.includes(local) && isAutoPoolMember(local))
|
|
94
132
|
pool.push(local);
|
|
133
|
+
if (pool.length === 0)
|
|
134
|
+
throw new Error(formatEmptyAutoPoolError());
|
|
95
135
|
const signals = await (opts.probe ?? probePoolSignals)(pool, agent);
|
|
96
136
|
if (!agent && !opts.probe) {
|
|
97
137
|
const { collectFleetHarnesses } = await import('../commands/ssh.js');
|
|
@@ -135,6 +175,12 @@ export async function resolveDeviceAuto(agent, opts = {}) {
|
|
|
135
175
|
}
|
|
136
176
|
/**
|
|
137
177
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
178
|
+
*
|
|
179
|
+
* Draws from the same automatic-placement pool as {@link resolveDeviceAuto}, so
|
|
180
|
+
* `agents ssh auto`, the generic `--host auto` passthrough, and `matchHost`'s
|
|
181
|
+
* `auto` sentinel honour device roles too. Throws when roles leave the pool
|
|
182
|
+
* empty — a `null` host here means "run locally", which for a box marked
|
|
183
|
+
* `personal` is the outcome the mark exists to prevent.
|
|
138
184
|
*/
|
|
139
185
|
export function resolveDeviceAffinity(opts = {}) {
|
|
140
186
|
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
@@ -142,9 +188,18 @@ export function resolveDeviceAffinity(opts = {}) {
|
|
|
142
188
|
const rng = opts.rng ?? Math.random;
|
|
143
189
|
const sinceDays = opts.sinceDays ?? 14;
|
|
144
190
|
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
|
|
191
|
+
// `listOnlineDeviceNames` always contained the local machine before device
|
|
192
|
+
// roles existed, so an empty default list can only mean roles excluded
|
|
193
|
+
// everything — fail loud, exactly as resolveDeviceAuto does. An explicitly
|
|
194
|
+
// empty `eligibleHosts` is the caller's own list and keeps the historical
|
|
195
|
+
// degrade-to-local behavior.
|
|
196
|
+
const usingDefaultPool = opts.eligibleHosts === undefined;
|
|
145
197
|
const eligible = new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost));
|
|
146
|
-
if (eligible.size === 0)
|
|
198
|
+
if (eligible.size === 0) {
|
|
199
|
+
if (usingDefaultPool)
|
|
200
|
+
throw new Error(formatEmptyAutoPoolError());
|
|
147
201
|
eligible.add(local);
|
|
202
|
+
}
|
|
148
203
|
const deviceRows = opts.deviceAffinity ??
|
|
149
204
|
queryAffinityRollup({
|
|
150
205
|
groupBy: 'machine',
|