@phnx-labs/agents-cli 1.20.93 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +264 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +7 -0
- package/dist/lib/activity.js +19 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- 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/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +17 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device/user config keys — typed read/write over the two-tier agents.yaml store.
|
|
3
|
+
*
|
|
4
|
+
* One registry (`CONFIG_KEYS`) maps each CLI dotted name to where it lives:
|
|
5
|
+
* - user scope → central `~/.agents/agents.yaml` under `config:` (syncs
|
|
6
|
+
* fleet-wide via `agents repo push/pull`)
|
|
7
|
+
* - device scope → `~/.agents/devices/<host>/agents.yaml` under `config:`
|
|
8
|
+
* (per-machine; mirrors how `defaultBrowserProfile` is routed)
|
|
9
|
+
*
|
|
10
|
+
* This machine's keys go through the readMeta/updateMeta funnel (state.ts) so the
|
|
11
|
+
* partition/overlay logic stays the single writer. Another device's doc is
|
|
12
|
+
* read/written in place — the devices/ tree syncs via the DotAgents repo, so
|
|
13
|
+
* editing `devices/mac-mini/agents.yaml` locally is how `configure`/`note`
|
|
14
|
+
* target a peer (`--device`-style).
|
|
15
|
+
*
|
|
16
|
+
* `browser.profile` is NOT a `config:` key — it is the existing
|
|
17
|
+
* `Meta.defaultBrowserProfile` field; the registry entry documents the mapping
|
|
18
|
+
* and set/get route to it so there is one source of truth (no duplicate key).
|
|
19
|
+
*
|
|
20
|
+
* Unset always means today's behavior.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import * as yaml from 'yaml';
|
|
25
|
+
import { META_HEADER, getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
26
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
27
|
+
import { machineId } from './machine-id.js';
|
|
28
|
+
import { assertValidDeviceName } from './devices/registry.js';
|
|
29
|
+
export const CONFIG_KEYS = [
|
|
30
|
+
{
|
|
31
|
+
name: 'interactive.host',
|
|
32
|
+
yamlKey: 'interactiveHost',
|
|
33
|
+
scope: 'user',
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Device that shows the user artifacts (browser opens, dashboards) — the "online macOS box" skills should use instead of guessing.',
|
|
36
|
+
validate: (v) => {
|
|
37
|
+
try {
|
|
38
|
+
assertValidDeviceName(v);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return err?.message ?? String(err);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'browser.profile',
|
|
48
|
+
yamlKey: 'defaultBrowserProfile',
|
|
49
|
+
scope: 'device',
|
|
50
|
+
type: 'string',
|
|
51
|
+
field: 'defaultBrowserProfile',
|
|
52
|
+
description: 'Browser profile `agents browser start` resolves to without --profile (set via `agents browser profiles set-default`).',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'agents.max-concurrent',
|
|
56
|
+
yamlKey: 'maxAgents',
|
|
57
|
+
scope: 'device',
|
|
58
|
+
type: 'int',
|
|
59
|
+
description: 'Cap on concurrent agents on this device. What counts toward it depends on the consumer: ' +
|
|
60
|
+
'Factory auto-launch counts device-wide running agents; teams placement counts the team’s own roster on the device.',
|
|
61
|
+
validate: (v) => (v >= 1 ? null : 'agents.max-concurrent must be >= 1.'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'scheduler.enabled',
|
|
65
|
+
yamlKey: 'schedulerEnabled',
|
|
66
|
+
scope: 'device',
|
|
67
|
+
type: 'bool',
|
|
68
|
+
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'notes',
|
|
72
|
+
yamlKey: 'notes',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'string-list',
|
|
75
|
+
description: 'Free-form operator notes about this device (one entry per `agents devices note`).',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
79
|
+
export function configKeySpec(name) {
|
|
80
|
+
const spec = CONFIG_KEYS.find((k) => k.name === name);
|
|
81
|
+
if (!spec) {
|
|
82
|
+
throw new Error(`Unknown config key '${name}'. Known keys: ${CONFIG_KEYS.map((k) => k.name).join(', ')}.`);
|
|
83
|
+
}
|
|
84
|
+
return spec;
|
|
85
|
+
}
|
|
86
|
+
/** Throw when `value` does not match the key's declared type or validation. */
|
|
87
|
+
function assertValidValue(spec, value) {
|
|
88
|
+
switch (spec.type) {
|
|
89
|
+
case 'string':
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
91
|
+
throw new Error(`Config key '${spec.name}' expects a non-empty string, got ${JSON.stringify(value)}.`);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'int':
|
|
95
|
+
if (!Number.isInteger(value)) {
|
|
96
|
+
throw new Error(`Config key '${spec.name}' expects an integer, got ${JSON.stringify(value)}.`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'bool':
|
|
100
|
+
if (typeof value !== 'boolean') {
|
|
101
|
+
throw new Error(`Config key '${spec.name}' expects a boolean, got ${JSON.stringify(value)}.`);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'string-list':
|
|
105
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
|
|
106
|
+
throw new Error(`Config key '${spec.name}' expects a list of strings, got ${JSON.stringify(value)}.`);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const err = spec.validate?.(value);
|
|
111
|
+
if (err)
|
|
112
|
+
throw new Error(`Invalid value for '${spec.name}': ${err}`);
|
|
113
|
+
}
|
|
114
|
+
// ─── Sibling-device doc access ────────────────────────────────────────────────
|
|
115
|
+
/** Path to any device's doc (self or a peer) under the synced devices/ tree. */
|
|
116
|
+
function deviceDocPath(device) {
|
|
117
|
+
return path.join(getUserAgentsDir(), 'devices', device, 'agents.yaml');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read a device doc directly. Returns null when the file does not exist. A
|
|
121
|
+
* malformed file is a hard error — silently returning null would let the next
|
|
122
|
+
* write wipe the device's pins/default (same contract as the device registry).
|
|
123
|
+
* A valid-but-non-map document (a bare string, a list) is the same kind of
|
|
124
|
+
* corruption: reject it here instead of failing later with a TypeError.
|
|
125
|
+
*/
|
|
126
|
+
function readDeviceDoc(device) {
|
|
127
|
+
const p = deviceDocPath(device);
|
|
128
|
+
let raw;
|
|
129
|
+
try {
|
|
130
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err && err.code === 'ENOENT')
|
|
134
|
+
return null;
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
const corrupted = (detail) => new Error(`Device config corrupted at ${p}: ${detail}. Inspect and restore from backup.`);
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = yaml.parse(raw);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
throw corrupted(err?.message ?? String(err));
|
|
144
|
+
}
|
|
145
|
+
if (parsed === null || parsed === undefined)
|
|
146
|
+
return {};
|
|
147
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
148
|
+
throw corrupted(`expected a YAML map, got ${Array.isArray(parsed) ? 'a list' : JSON.stringify(parsed)}`);
|
|
149
|
+
}
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
/** Write a device doc in place (atomic). Only ever holds device-local fields. */
|
|
153
|
+
function writeDeviceDoc(device, doc) {
|
|
154
|
+
const p = deviceDocPath(device);
|
|
155
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
156
|
+
const body = Object.keys(doc).length > 0 ? doc : { agents: {} };
|
|
157
|
+
atomicWriteFileSync(p, META_HEADER + yaml.stringify(body));
|
|
158
|
+
}
|
|
159
|
+
// ─── Reads ────────────────────────────────────────────────────────────────────
|
|
160
|
+
function entryFromMeta(spec, meta) {
|
|
161
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
162
|
+
const value = meta.defaultBrowserProfile;
|
|
163
|
+
return { spec, value, layer: value !== undefined ? 'device' : undefined };
|
|
164
|
+
}
|
|
165
|
+
if (spec.scope === 'user') {
|
|
166
|
+
const value = meta.config?.[spec.yamlKey];
|
|
167
|
+
return { spec, value, layer: value !== undefined ? 'user' : undefined };
|
|
168
|
+
}
|
|
169
|
+
const value = meta.deviceConfig?.[spec.yamlKey];
|
|
170
|
+
return { spec, value, layer: value !== undefined ? 'device' : undefined };
|
|
171
|
+
}
|
|
172
|
+
/** True when `device` names this machine (case-insensitive, mirroring
|
|
173
|
+
* `isLocalDevice` in teams/scheduler.ts) — `configure ZION` on host zion must
|
|
174
|
+
* take the self path (readMeta/updateMeta funnel), not the peer-doc path. */
|
|
175
|
+
function isSelfDevice(device) {
|
|
176
|
+
return device.toLowerCase() === machineId();
|
|
177
|
+
}
|
|
178
|
+
/** Get one config key's value and the layer that set it. */
|
|
179
|
+
export function getConfigValue(name, opts) {
|
|
180
|
+
const spec = configKeySpec(name);
|
|
181
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
182
|
+
const doc = readDeviceDoc(opts.device) ?? {};
|
|
183
|
+
return entryFromMeta(spec, { deviceConfig: doc.config, defaultBrowserProfile: doc.defaultBrowserProfile });
|
|
184
|
+
}
|
|
185
|
+
return entryFromMeta(spec, readMeta());
|
|
186
|
+
}
|
|
187
|
+
/** List every known key with its value and the layer that set it. */
|
|
188
|
+
export function listConfig(opts) {
|
|
189
|
+
return CONFIG_KEYS.map((spec) => getConfigValue(spec.name, opts));
|
|
190
|
+
}
|
|
191
|
+
// ─── Writes ───────────────────────────────────────────────────────────────────
|
|
192
|
+
function setInMeta(spec, value) {
|
|
193
|
+
updateMeta((m) => {
|
|
194
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
195
|
+
return { ...m, defaultBrowserProfile: value };
|
|
196
|
+
}
|
|
197
|
+
if (spec.scope === 'user') {
|
|
198
|
+
return { ...m, config: { ...m.config, [spec.yamlKey]: value } };
|
|
199
|
+
}
|
|
200
|
+
return { ...m, deviceConfig: { ...m.deviceConfig, [spec.yamlKey]: value } };
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function unsetInMeta(spec) {
|
|
204
|
+
updateMeta((m) => {
|
|
205
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
206
|
+
const { defaultBrowserProfile, ...rest } = m;
|
|
207
|
+
void defaultBrowserProfile;
|
|
208
|
+
return rest;
|
|
209
|
+
}
|
|
210
|
+
const block = spec.scope === 'user' ? m.config : m.deviceConfig;
|
|
211
|
+
if (!block || !(spec.yamlKey in block))
|
|
212
|
+
return m;
|
|
213
|
+
const next = { ...block };
|
|
214
|
+
delete next[spec.yamlKey];
|
|
215
|
+
const cleaned = Object.keys(next).length > 0 ? next : undefined;
|
|
216
|
+
return spec.scope === 'user' ? { ...m, config: cleaned } : { ...m, deviceConfig: cleaned };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function setInDeviceDoc(device, spec, value) {
|
|
220
|
+
const doc = readDeviceDoc(device) ?? {};
|
|
221
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
222
|
+
doc.defaultBrowserProfile = value;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
doc.config = { ...doc.config, [spec.yamlKey]: value };
|
|
226
|
+
}
|
|
227
|
+
writeDeviceDoc(device, doc);
|
|
228
|
+
}
|
|
229
|
+
function unsetInDeviceDoc(device, spec) {
|
|
230
|
+
const doc = readDeviceDoc(device);
|
|
231
|
+
if (!doc)
|
|
232
|
+
return; // nothing stored — unset is a no-op
|
|
233
|
+
if (spec.field === 'defaultBrowserProfile') {
|
|
234
|
+
delete doc.defaultBrowserProfile;
|
|
235
|
+
}
|
|
236
|
+
else if (doc.config && spec.yamlKey in doc.config) {
|
|
237
|
+
delete doc.config[spec.yamlKey];
|
|
238
|
+
if (Object.keys(doc.config).length === 0)
|
|
239
|
+
delete doc.config;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
return; // key not present — no write needed
|
|
243
|
+
}
|
|
244
|
+
writeDeviceDoc(device, doc);
|
|
245
|
+
}
|
|
246
|
+
/** Set a config key (validated). Device-scope keys target this machine unless `opts.device` names a peer. */
|
|
247
|
+
export function setConfigValue(name, value, opts) {
|
|
248
|
+
const spec = configKeySpec(name);
|
|
249
|
+
assertValidValue(spec, value);
|
|
250
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
251
|
+
setInDeviceDoc(opts.device, spec, value);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
setInMeta(spec, value);
|
|
255
|
+
}
|
|
256
|
+
/** Unset a config key — restores default behavior. No-op when already unset. */
|
|
257
|
+
export function unsetConfigValue(name, opts) {
|
|
258
|
+
const spec = configKeySpec(name);
|
|
259
|
+
if (spec.scope === 'device' && opts?.device && !isSelfDevice(opts.device)) {
|
|
260
|
+
unsetInDeviceDoc(opts.device, spec);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
unsetInMeta(spec);
|
|
264
|
+
}
|
|
265
|
+
// ─── Consumers' helpers ───────────────────────────────────────────────────────
|
|
266
|
+
/** True unless this machine's device doc disables the routines scheduler. */
|
|
267
|
+
export function isSchedulerEnabled() {
|
|
268
|
+
return getConfigValue('scheduler.enabled').value !== false;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Throw when the routines scheduler is disabled on this machine, naming the
|
|
272
|
+
* setting and the fix. The single message every scheduler-start surface
|
|
273
|
+
* (auto-start on `routines add`, manual `routines start`, the daemon's own
|
|
274
|
+
* scheduler init) refuses with.
|
|
275
|
+
*/
|
|
276
|
+
export function assertSchedulerEnabled() {
|
|
277
|
+
if (isSchedulerEnabled())
|
|
278
|
+
return;
|
|
279
|
+
throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
|
|
280
|
+
`Re-enable with: agents devices configure ${machineId()} --scheduler on`);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the `agents.max-concurrent` cap for each named device from its synced
|
|
284
|
+
* device doc (no SSH). Devices without a cap are omitted — uncapped is the
|
|
285
|
+
* default. Used as an input to host ranking (teams placement, Factory
|
|
286
|
+
* auto-launch), never as a remote probe.
|
|
287
|
+
*/
|
|
288
|
+
export function readMaxConcurrentCaps(devices) {
|
|
289
|
+
const caps = {};
|
|
290
|
+
for (const device of devices) {
|
|
291
|
+
const value = getConfigValue('agents.max-concurrent', { device }).value;
|
|
292
|
+
if (typeof value === 'number')
|
|
293
|
+
caps[device] = value;
|
|
294
|
+
}
|
|
295
|
+
return caps;
|
|
296
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ActorKind } from './actor.js';
|
|
2
|
+
export interface EventProvenance {
|
|
3
|
+
osUser: string;
|
|
4
|
+
transport: 'local' | 'ssh';
|
|
5
|
+
sshClientIp?: string;
|
|
6
|
+
actor: string;
|
|
7
|
+
kind: ActorKind;
|
|
8
|
+
machineId: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
agent?: string;
|
|
11
|
+
launchId?: string;
|
|
12
|
+
parentSessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Stamp the shared identity floor used by both operational and activity events.
|
|
16
|
+
* Explicit event payload fields may override these defaults at the call site.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stampProvenance(env?: NodeJS.ProcessEnv): EventProvenance;
|
|
19
|
+
export declare function resetEventProvenanceForTest(): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { resolveActor } from './actor.js';
|
|
3
|
+
import { machineId } from './machine-id.js';
|
|
4
|
+
import { parseSshConnection } from './session/provenance.js';
|
|
5
|
+
let cachedOrigin;
|
|
6
|
+
let cachedDeviceId;
|
|
7
|
+
/**
|
|
8
|
+
* Stamp the shared identity floor used by both operational and activity events.
|
|
9
|
+
* Explicit event payload fields may override these defaults at the call site.
|
|
10
|
+
*/
|
|
11
|
+
export function stampProvenance(env = process.env) {
|
|
12
|
+
if (!cachedOrigin) {
|
|
13
|
+
let osUser = 'unknown';
|
|
14
|
+
try {
|
|
15
|
+
osUser = os.userInfo().username;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// A uid without a passwd entry has no attributable OS user.
|
|
19
|
+
}
|
|
20
|
+
const ssh = env.SSH_CONNECTION ? parseSshConnection(env.SSH_CONNECTION) : undefined;
|
|
21
|
+
const actor = resolveActor();
|
|
22
|
+
cachedOrigin = {
|
|
23
|
+
osUser,
|
|
24
|
+
transport: ssh ? 'ssh' : 'local',
|
|
25
|
+
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
26
|
+
actor: actor.id,
|
|
27
|
+
kind: actor.kind,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const provenance = {
|
|
31
|
+
...cachedOrigin,
|
|
32
|
+
machineId: (cachedDeviceId ??= machineId()),
|
|
33
|
+
};
|
|
34
|
+
const sessionId = env.AGENT_SESSION_ID || env.AGENTS_SESSION_ID;
|
|
35
|
+
if (sessionId)
|
|
36
|
+
provenance.sessionId = sessionId;
|
|
37
|
+
if (env.AGENTS_AGENT_NAME)
|
|
38
|
+
provenance.agent = env.AGENTS_AGENT_NAME;
|
|
39
|
+
if (env.AGENT_LAUNCH_ID)
|
|
40
|
+
provenance.launchId = env.AGENT_LAUNCH_ID;
|
|
41
|
+
if (env.AGENTS_PARENT_SESSION_ID)
|
|
42
|
+
provenance.parentSessionId = env.AGENTS_PARENT_SESSION_ID;
|
|
43
|
+
return provenance;
|
|
44
|
+
}
|
|
45
|
+
export function resetEventProvenanceForTest() {
|
|
46
|
+
cachedOrigin = undefined;
|
|
47
|
+
cachedDeviceId = undefined;
|
|
48
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import type { ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
16
|
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
@@ -46,7 +46,7 @@ export interface EventMeta {
|
|
|
46
46
|
/** Resolved actor id — which human/agent is behind this event (RUSH-2020). */
|
|
47
47
|
actor?: string;
|
|
48
48
|
/** Actor kind (`human`/`agent`). */
|
|
49
|
-
kind?: ActorKind;
|
|
49
|
+
kind?: ActorKind | 'unknown';
|
|
50
50
|
}
|
|
51
51
|
export interface EventPayload {
|
|
52
52
|
agent?: string;
|
package/dist/lib/events.js
CHANGED
|
@@ -16,11 +16,9 @@ import * as path from 'path';
|
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
19
|
-
import { parseSshConnection } from './session/provenance.js';
|
|
20
19
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
21
20
|
import { getUserAgentsDir } from './state.js';
|
|
22
|
-
import {
|
|
23
|
-
import { machineId } from './machine-id.js';
|
|
21
|
+
import { stampProvenance, resetEventProvenanceForTest } from './event-provenance.js';
|
|
24
22
|
/** Lazy perf warehouse write — avoids a hard cycle at module load. */
|
|
25
23
|
function recordPerfTiming(payload) {
|
|
26
24
|
try {
|
|
@@ -329,51 +327,7 @@ export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.st
|
|
|
329
327
|
}
|
|
330
328
|
return { kind: stdoutIsTTY ? 'terminal' : 'script' };
|
|
331
329
|
}
|
|
332
|
-
|
|
333
|
-
* Who is running this process and from where. Derived once per process from the
|
|
334
|
-
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
335
|
-
* cached — provenance can't change mid-process, so every emit() pays for it once.
|
|
336
|
-
*/
|
|
337
|
-
let _origin;
|
|
338
|
-
function auditOrigin() {
|
|
339
|
-
if (_origin)
|
|
340
|
-
return _origin;
|
|
341
|
-
let osUser = 'unknown';
|
|
342
|
-
try {
|
|
343
|
-
osUser = os.userInfo().username;
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
// Container/edge cases where the uid has no passwd entry.
|
|
347
|
-
}
|
|
348
|
-
const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
|
|
349
|
-
const actor = resolveActor();
|
|
350
|
-
_origin = {
|
|
351
|
-
osUser,
|
|
352
|
-
transport: ssh ? 'ssh' : 'local',
|
|
353
|
-
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
354
|
-
actor: actor.id,
|
|
355
|
-
kind: actor.kind,
|
|
356
|
-
};
|
|
357
|
-
return _origin;
|
|
358
|
-
}
|
|
359
|
-
/** This machine's normalized device id, resolved once — it can't change mid-process. */
|
|
360
|
-
let _machineId;
|
|
361
|
-
function cachedMachineId() {
|
|
362
|
-
return (_machineId ??= machineId());
|
|
363
|
-
}
|
|
364
|
-
function resolveProvenance(env = process.env) {
|
|
365
|
-
const p = {};
|
|
366
|
-
const sessionId = env.AGENT_SESSION_ID || env.AGENTS_SESSION_ID;
|
|
367
|
-
if (sessionId)
|
|
368
|
-
p.sessionId = sessionId;
|
|
369
|
-
if (env.AGENTS_AGENT_NAME)
|
|
370
|
-
p.agent = env.AGENTS_AGENT_NAME;
|
|
371
|
-
if (env.AGENT_LAUNCH_ID)
|
|
372
|
-
p.launchId = env.AGENT_LAUNCH_ID;
|
|
373
|
-
if (env.AGENTS_PARENT_SESSION_ID)
|
|
374
|
-
p.parentSessionId = env.AGENTS_PARENT_SESSION_ID;
|
|
375
|
-
return p;
|
|
376
|
-
}
|
|
330
|
+
// ─── Audit attribution ────────────────────────────────────────────────────────
|
|
377
331
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
378
332
|
/**
|
|
379
333
|
* Emit a structured event to the append-only audit log.
|
|
@@ -396,13 +350,12 @@ export function emit(event, payload = {}, overrides = {}) {
|
|
|
396
350
|
const safePayload = sanitizePayload(payload);
|
|
397
351
|
const record = {
|
|
398
352
|
// Provenance floor first: env-sourced defaults an explicit payload overrides.
|
|
399
|
-
...
|
|
353
|
+
...stampProvenance(),
|
|
400
354
|
...safePayload,
|
|
401
355
|
ts: overrides.ts ?? new Date().toISOString(),
|
|
402
356
|
tz: getTimezoneOffset(),
|
|
403
357
|
tzName: getTimezoneName(),
|
|
404
358
|
hostname: os.hostname(),
|
|
405
|
-
machineId: cachedMachineId(),
|
|
406
359
|
platform: os.platform(),
|
|
407
360
|
arch: os.arch(),
|
|
408
361
|
pid: process.pid,
|
|
@@ -411,7 +364,6 @@ export function emit(event, payload = {}, overrides = {}) {
|
|
|
411
364
|
level: levelFor(event),
|
|
412
365
|
caller: caller.kind,
|
|
413
366
|
...(caller.session ? { session: caller.session } : {}),
|
|
414
|
-
...auditOrigin(),
|
|
415
367
|
};
|
|
416
368
|
const line = JSON.stringify(record) + '\n';
|
|
417
369
|
const logPath = eventsPath();
|
|
@@ -919,8 +871,7 @@ export function getLogsPath() {
|
|
|
919
871
|
}
|
|
920
872
|
export function _resetForTest(overrideEventsPath) {
|
|
921
873
|
_eventsPath = overrideEventsPath;
|
|
922
|
-
|
|
923
|
-
_machineId = undefined;
|
|
874
|
+
resetEventProvenanceForTest();
|
|
924
875
|
_chmoddedPath = undefined;
|
|
925
876
|
lastRotationCheck = 0;
|
|
926
877
|
}
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface MessageReceipt {
|
|
|
12
12
|
/** The message id this receipt describes. */
|
|
13
13
|
msgId: string;
|
|
14
14
|
/** Delivery lifecycle state. */
|
|
15
|
-
status: 'queued' | 'consumed' | 'continued';
|
|
15
|
+
status: 'queued' | 'consumed' | 'continued' | 'dropped' | 'expired';
|
|
16
16
|
/** ISO-8601 timestamp of the state transition. */
|
|
17
17
|
at: string;
|
|
18
18
|
/** Optional sender label for the message. */
|
package/dist/lib/feed.js
CHANGED
package/dist/lib/git.d.ts
CHANGED
|
@@ -41,8 +41,12 @@ export declare function assertValidBranchName(branch: string): void;
|
|
|
41
41
|
*
|
|
42
42
|
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
43
43
|
* repo state rather than a hard-coded literal.
|
|
44
|
+
*
|
|
45
|
+
* Pass `targetBranch` to push the local `branch` to a differently-named remote
|
|
46
|
+
* branch (`git push origin <branch>:<targetBranch>`) — used when publishing the
|
|
47
|
+
* working tree to a branch other than the checked-out one.
|
|
44
48
|
*/
|
|
45
|
-
export declare function pushOrigin(git: SimpleGit, branch: string): Promise<void>;
|
|
49
|
+
export declare function pushOrigin(git: SimpleGit, branch: string, targetBranch?: string): Promise<void>;
|
|
46
50
|
/** Parsed representation of a git source string (GitHub, generic URL, or local path). */
|
|
47
51
|
export interface GitSource {
|
|
48
52
|
type: 'github' | 'url' | 'local';
|
|
@@ -124,6 +128,8 @@ export declare function resolveGitHubUsername(): Promise<string | null>;
|
|
|
124
128
|
* Get the remote URL for origin in a git repo.
|
|
125
129
|
*/
|
|
126
130
|
export declare function getRemoteUrl(repoPath: string): Promise<string | null>;
|
|
131
|
+
/** The repo's checked-out branch, or 'main' on a detached HEAD / read failure. */
|
|
132
|
+
export declare function getCurrentBranch(repoPath: string): Promise<string>;
|
|
127
133
|
/**
|
|
128
134
|
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
129
135
|
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
@@ -158,8 +164,13 @@ export type CommitAndPushResult = {
|
|
|
158
164
|
* Clean tree + local ahead of origin still pushes — "nothing to commit" is not
|
|
159
165
|
* "nothing to push". Reports "already up to date" only when `ahead === 0` and
|
|
160
166
|
* there is nothing to commit.
|
|
167
|
+
*
|
|
168
|
+
* `targetBranch` pushes the working tree to a differently-named remote branch
|
|
169
|
+
* (`<current>:<targetBranch>`) and is reported back as the result `branch`, so
|
|
170
|
+
* callers that print a branch-scoped URL reference where the commit actually
|
|
171
|
+
* landed — not the checked-out branch.
|
|
161
172
|
*/
|
|
162
|
-
export declare function commitAndPush(repoPath: string, message: string): Promise<CommitAndPushResult>;
|
|
173
|
+
export declare function commitAndPush(repoPath: string, message: string, targetBranch?: string): Promise<CommitAndPushResult>;
|
|
163
174
|
/**
|
|
164
175
|
* Check if repo has uncommitted changes.
|
|
165
176
|
*/
|
package/dist/lib/git.js
CHANGED
|
@@ -77,9 +77,18 @@ export function assertValidBranchName(branch) {
|
|
|
77
77
|
*
|
|
78
78
|
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
79
79
|
* repo state rather than a hard-coded literal.
|
|
80
|
+
*
|
|
81
|
+
* Pass `targetBranch` to push the local `branch` to a differently-named remote
|
|
82
|
+
* branch (`git push origin <branch>:<targetBranch>`) — used when publishing the
|
|
83
|
+
* working tree to a branch other than the checked-out one.
|
|
80
84
|
*/
|
|
81
|
-
export async function pushOrigin(git, branch) {
|
|
85
|
+
export async function pushOrigin(git, branch, targetBranch) {
|
|
82
86
|
assertValidBranchName(branch);
|
|
87
|
+
if (targetBranch && targetBranch !== branch) {
|
|
88
|
+
assertValidBranchName(targetBranch);
|
|
89
|
+
await git.raw(['push', '--', 'origin', `${branch}:${targetBranch}`]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
83
92
|
await git.raw(['push', '--', 'origin', branch]);
|
|
84
93
|
}
|
|
85
94
|
/**
|
|
@@ -435,6 +444,16 @@ export async function getRemoteUrl(repoPath) {
|
|
|
435
444
|
return null;
|
|
436
445
|
}
|
|
437
446
|
}
|
|
447
|
+
/** The repo's checked-out branch, or 'main' on a detached HEAD / read failure. */
|
|
448
|
+
export async function getCurrentBranch(repoPath) {
|
|
449
|
+
try {
|
|
450
|
+
const status = await simpleGit(repoPath).status();
|
|
451
|
+
return status.current || 'main';
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return 'main';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
438
457
|
/**
|
|
439
458
|
* Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
|
|
440
459
|
* same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
|
|
@@ -494,13 +513,23 @@ export async function checkGitHubRepoExists(owner, repo) {
|
|
|
494
513
|
* Clean tree + local ahead of origin still pushes — "nothing to commit" is not
|
|
495
514
|
* "nothing to push". Reports "already up to date" only when `ahead === 0` and
|
|
496
515
|
* there is nothing to commit.
|
|
516
|
+
*
|
|
517
|
+
* `targetBranch` pushes the working tree to a differently-named remote branch
|
|
518
|
+
* (`<current>:<targetBranch>`) and is reported back as the result `branch`, so
|
|
519
|
+
* callers that print a branch-scoped URL reference where the commit actually
|
|
520
|
+
* landed — not the checked-out branch.
|
|
497
521
|
*/
|
|
498
|
-
export async function commitAndPush(repoPath, message) {
|
|
522
|
+
export async function commitAndPush(repoPath, message, targetBranch) {
|
|
499
523
|
try {
|
|
500
524
|
const git = simpleGit(repoPath);
|
|
501
525
|
let status = await git.status();
|
|
502
526
|
const branch = status.current || 'main';
|
|
503
527
|
assertValidBranchName(branch);
|
|
528
|
+
if (targetBranch)
|
|
529
|
+
assertValidBranchName(targetBranch);
|
|
530
|
+
// The branch the commit ends up on remotely — the checked-out branch unless
|
|
531
|
+
// an explicit target was requested.
|
|
532
|
+
const pushedBranch = targetBranch || branch;
|
|
504
533
|
let committed = false;
|
|
505
534
|
if (status.files.length > 0) {
|
|
506
535
|
await git.add('-A');
|
|
@@ -509,7 +538,10 @@ export async function commitAndPush(repoPath, message) {
|
|
|
509
538
|
status = await git.status();
|
|
510
539
|
}
|
|
511
540
|
const ahead = status.ahead ?? 0;
|
|
512
|
-
|
|
541
|
+
// A same-branch push short-circuits when there is nothing new; a push to a
|
|
542
|
+
// different target branch must still run even from a clean, non-ahead tree,
|
|
543
|
+
// since the target may not carry these commits yet.
|
|
544
|
+
if (!committed && ahead === 0 && pushedBranch === branch) {
|
|
513
545
|
return {
|
|
514
546
|
success: true,
|
|
515
547
|
detail: 'already up to date',
|
|
@@ -521,12 +553,12 @@ export async function commitAndPush(repoPath, message) {
|
|
|
521
553
|
// Capture remote tip before push for a real ref range in the detail string.
|
|
522
554
|
let before = '';
|
|
523
555
|
try {
|
|
524
|
-
before = (await git.raw(['rev-parse', '--short=8', `origin/${
|
|
556
|
+
before = (await git.raw(['rev-parse', '--short=8', `origin/${pushedBranch}`])).trim();
|
|
525
557
|
}
|
|
526
558
|
catch {
|
|
527
559
|
/* origin/<branch> may not exist yet (first push) */
|
|
528
560
|
}
|
|
529
|
-
await pushOrigin(git, branch);
|
|
561
|
+
await pushOrigin(git, branch, targetBranch);
|
|
530
562
|
let after = '';
|
|
531
563
|
try {
|
|
532
564
|
after = (await git.raw(['rev-parse', '--short=8', 'HEAD'])).trim();
|
|
@@ -547,7 +579,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
547
579
|
return {
|
|
548
580
|
success: true,
|
|
549
581
|
detail,
|
|
550
|
-
branch,
|
|
582
|
+
branch: pushedBranch,
|
|
551
583
|
committed,
|
|
552
584
|
pushed: true,
|
|
553
585
|
};
|