@aiaiai-pt/martha-cli 0.58.0 → 0.61.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 +54 -0
- package/dist/index.js +263 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project adheres to semver — `0.x` releases may include breaking changes between minor versions.
|
|
4
4
|
|
|
5
|
+
## [0.61.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Added — `martha spend` and `martha budget` (#1107)
|
|
8
|
+
|
|
9
|
+
- `martha spend [--tenant <id>] [--group-by day|agent|model|surface|billing_owner] [--since] [--until]`
|
|
10
|
+
reads a tenant's LLM spend from Martha's own ledger (`GET /api/admin/tenants/{t}/spend`).
|
|
11
|
+
- `martha budget list|set|rm` manages per-tenant and per-AI-Agent LLM spend ceilings
|
|
12
|
+
(`warn`, `require_approval` — the run suspends until an ApprovalCase is approved — or `block`).
|
|
13
|
+
|
|
14
|
+
Both default to your own tenant; another tenant needs super-admin (the API answers 403).
|
|
15
|
+
|
|
16
|
+
### Fixed — commands that default to "your tenant" on realms that use Keycloak Organizations
|
|
17
|
+
|
|
18
|
+
On realms that model tenants as Keycloak Organizations (ubiwhere.io) the access token carries
|
|
19
|
+
an `organizations` claim and no `tenant_id`, so every command that defaults to the caller's
|
|
20
|
+
tenant failed with `No tenant_id claim in access token`. The CLI now resolves the tenant exactly
|
|
21
|
+
as the API does: `tenant_id`, else the first organization.
|
|
22
|
+
|
|
23
|
+
## [0.60.0] — 2026-08-20
|
|
24
|
+
|
|
25
|
+
### Added — `kind: ClientAccess`, so a declared workflow can also be authorized
|
|
26
|
+
|
|
27
|
+
`definitions apply` could declare a Function, a Workflow, an Agent and a
|
|
28
|
+
Trigger. It could not declare the GRANTS that let a principal call them —
|
|
29
|
+
those existed only as `martha clients grant`. So anything that COMPILES a
|
|
30
|
+
workflow installed the workflow without the authority to run it, and the gap
|
|
31
|
+
only showed at run time as `Unknown function: <name>` on a function that is
|
|
32
|
+
registered and active for the tenant.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
kind: ClientAccess
|
|
36
|
+
name: martha-cli-zero # client_key, name or id — what `martha clients` takes
|
|
37
|
+
grants:
|
|
38
|
+
- { type: function, name: get_entity_entities_type_code_entity_id_get }
|
|
39
|
+
- { type: workflow, name: some_workflow }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Additive, like the rest of apply: a document that drops a grant does not revoke
|
|
43
|
+
it. `ClientAccess` documents are applied LAST within a run, so one file can
|
|
44
|
+
carry a workflow and the authority to run it in any order.
|
|
45
|
+
|
|
46
|
+
## [0.58.1] — 2026-08-04
|
|
47
|
+
|
|
48
|
+
### Fixed — provisioning called an existing agent blind when it wasn't
|
|
49
|
+
|
|
50
|
+
0.57.0 taught `bootstrap --mode agent` to say `granted NO collections — its
|
|
51
|
+
retrieval tools will return nothing`. True for an agent created by that call;
|
|
52
|
+
false for one that already existed, whose grants predate it. The honest
|
|
53
|
+
statement for an adopted agent is that this call granted nothing *new* and left
|
|
54
|
+
its access alone, so that is what it says now — and the "will get an empty
|
|
55
|
+
list" warning is limited to agents we just created and therefore know are
|
|
56
|
+
empty. Reporting a false alarm is the same failure as reporting nothing, in the
|
|
57
|
+
other direction.
|
|
58
|
+
|
|
5
59
|
## [0.58.0] — 2026-08-04
|
|
6
60
|
|
|
7
61
|
### Fixed — the harness pointed strangers at a 404
|
package/dist/index.js
CHANGED
|
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
function __accessProp(key) {
|
|
9
|
-
return this[key];
|
|
10
|
-
}
|
|
11
|
-
var __toESMCache_node;
|
|
12
|
-
var __toESMCache_esm;
|
|
13
8
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
-
var canCache = mod != null && typeof mod === "object";
|
|
15
|
-
if (canCache) {
|
|
16
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
-
var cached = cache.get(mod);
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __returnValue = (v) => v;
|
|
35
|
-
function __exportSetter(name, newValue) {
|
|
36
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -1019,7 +1001,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1019
1001
|
this._exitCallback = (err) => {
|
|
1020
1002
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1021
1003
|
throw err;
|
|
1022
|
-
}
|
|
1004
|
+
} else {}
|
|
1023
1005
|
};
|
|
1024
1006
|
}
|
|
1025
1007
|
return this;
|
|
@@ -10753,6 +10735,18 @@ function belongsToClient(tokens, clientId) {
|
|
|
10753
10735
|
const azp = claims?.azp ?? claims?.client_id;
|
|
10754
10736
|
return azp ? azp === clientId : true;
|
|
10755
10737
|
}
|
|
10738
|
+
function tenantFromClaims(payload) {
|
|
10739
|
+
const tid = payload.tenant_id;
|
|
10740
|
+
if (typeof tid === "string" && tid)
|
|
10741
|
+
return tid;
|
|
10742
|
+
const orgs = payload.organizations;
|
|
10743
|
+
if (orgs && typeof orgs === "object" && !Array.isArray(orgs)) {
|
|
10744
|
+
const first = Object.keys(orgs)[0];
|
|
10745
|
+
if (first)
|
|
10746
|
+
return first;
|
|
10747
|
+
}
|
|
10748
|
+
return null;
|
|
10749
|
+
}
|
|
10756
10750
|
function createContext(opts) {
|
|
10757
10751
|
const config = loadConfig(opts.configDir);
|
|
10758
10752
|
const profileName = opts.profileOverride ?? process.env.MARTHA_PROFILE ?? config.current_profile;
|
|
@@ -10811,10 +10805,9 @@ function createContext(opts) {
|
|
|
10811
10805
|
});
|
|
10812
10806
|
const getTenantId = async () => {
|
|
10813
10807
|
const token = await getAccessToken();
|
|
10814
|
-
const
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
throw new CLIError("No tenant_id claim in access token.", 2 /* Auth */);
|
|
10808
|
+
const tid = tenantFromClaims(decodeJwtPayload(token));
|
|
10809
|
+
if (!tid) {
|
|
10810
|
+
throw new CLIError("No tenant_id or organizations claim in access token.", 2 /* Auth */);
|
|
10818
10811
|
}
|
|
10819
10812
|
return tid;
|
|
10820
10813
|
};
|
|
@@ -11149,7 +11142,7 @@ init_config();
|
|
|
11149
11142
|
init_errors();
|
|
11150
11143
|
|
|
11151
11144
|
// src/version.ts
|
|
11152
|
-
var CLI_VERSION = "0.
|
|
11145
|
+
var CLI_VERSION = "0.61.0";
|
|
11153
11146
|
|
|
11154
11147
|
// src/commands/sessions.ts
|
|
11155
11148
|
function relativeTime(iso) {
|
|
@@ -12831,9 +12824,16 @@ class BrowserSession {
|
|
|
12831
12824
|
|
|
12832
12825
|
// src/lib/host-files.ts
|
|
12833
12826
|
import { promises as fs6 } from "node:fs";
|
|
12827
|
+
import * as os3 from "node:os";
|
|
12834
12828
|
import * as path5 from "node:path";
|
|
12835
12829
|
var READ_CAP = 256 * 1024;
|
|
12836
12830
|
function resolvePath(cwd, p) {
|
|
12831
|
+
if (p === "~") {
|
|
12832
|
+
return os3.homedir();
|
|
12833
|
+
}
|
|
12834
|
+
if (p.startsWith("~/")) {
|
|
12835
|
+
return path5.join(os3.homedir(), p.slice(2));
|
|
12836
|
+
}
|
|
12837
12837
|
return path5.isAbsolute(p) ? p : path5.resolve(cwd, p);
|
|
12838
12838
|
}
|
|
12839
12839
|
async function applyFileAction(cwd, req) {
|
|
@@ -14621,16 +14621,17 @@ async function bootstrapAgentPrincipal(ctx, agent, opts = {}) {
|
|
|
14621
14621
|
throw new CLIError("Bootstrap did not return service-account credentials.", 1 /* Error */, "Check that your user has the admin realm role (integration mode requires it).");
|
|
14622
14622
|
}
|
|
14623
14623
|
const agentName = boot.agent?.name ?? agent;
|
|
14624
|
+
const agentAdopted = boot.agent?.adopted === true;
|
|
14624
14625
|
steps.push(boot.adopted ? `reusing principal '${boot.service_account.client_id}' (same secret — recovery, not rotation)` : `provisioned principal '${boot.service_account.client_id}'`);
|
|
14625
14626
|
steps.push(boot.agent?.adopted ? `bound existing agent '${agentName}' to client ${boot.client_id}` : `created agent '${agentName}' (runs_as_chat) and bound it to client ${boot.client_id}`);
|
|
14626
14627
|
const fns = boot.agent?.granted_functions ?? [];
|
|
14627
14628
|
steps.push(fns.length ? `granted its tools: ${fns.join(", ")}` : "granted NO tools — pass --intents rag (or documents/tasks) or it can do nothing");
|
|
14628
14629
|
const cols = boot.agent?.granted_collections ?? [];
|
|
14629
|
-
steps.push(cols.length ? `granted it read on: ${cols.join(", ")}` : "granted NO collections — its retrieval tools will return nothing until you pass --collections");
|
|
14630
|
+
steps.push(cols.length ? `granted it read on: ${cols.join(", ")}` : agentAdopted ? "granted no new collections (existing access unchanged)" : "granted NO collections — its retrieval tools will return nothing until you pass --collections");
|
|
14630
14631
|
if (!fns.length) {
|
|
14631
14632
|
warnings.push("no tools: this agent will answer from the model alone.");
|
|
14632
14633
|
}
|
|
14633
|
-
if (fns.length && !cols.length) {
|
|
14634
|
+
if (fns.length && !cols.length && !agentAdopted) {
|
|
14634
14635
|
warnings.push(`it can call list_collections and will get an empty list — grant one: martha collections grant <name> --to agent:${boot.agent?.agent_id ?? "<id>"}`);
|
|
14635
14636
|
}
|
|
14636
14637
|
for (const s of boot.skipped ?? []) {
|
|
@@ -14786,8 +14787,8 @@ function registerPrincipalCommands(program2) {
|
|
|
14786
14787
|
import { accessSync, constants, mkdirSync, writeFileSync } from "node:fs";
|
|
14787
14788
|
import { existsSync } from "node:fs";
|
|
14788
14789
|
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
14789
|
-
import { homedir } from "node:os";
|
|
14790
|
-
import { join as
|
|
14790
|
+
import { homedir as homedir2 } from "node:os";
|
|
14791
|
+
import { join as join4 } from "node:path";
|
|
14791
14792
|
init_errors();
|
|
14792
14793
|
var AGENTS_PATH2 = "/api/admin/definitions/agents";
|
|
14793
14794
|
var SUPPORTED_CHANNELS = ["buzz"];
|
|
@@ -14819,35 +14820,35 @@ function buzzAppDataCandidates(platform, env2, home) {
|
|
|
14819
14820
|
];
|
|
14820
14821
|
const base = () => {
|
|
14821
14822
|
if (platform === "darwin")
|
|
14822
|
-
return
|
|
14823
|
+
return join4(home, "Library", "Application Support");
|
|
14823
14824
|
if (platform === "win32")
|
|
14824
|
-
return env2.APPDATA ??
|
|
14825
|
-
return env2.XDG_DATA_HOME ??
|
|
14825
|
+
return env2.APPDATA ?? join4(home, "AppData", "Roaming");
|
|
14826
|
+
return env2.XDG_DATA_HOME ?? join4(home, ".local", "share");
|
|
14826
14827
|
};
|
|
14827
14828
|
const root = base();
|
|
14828
|
-
return ids.map(({ build, id }) => ({ build, dir:
|
|
14829
|
+
return ids.map(({ build, id }) => ({ build, dir: join4(root, id) }));
|
|
14829
14830
|
}
|
|
14830
14831
|
function chooseAppDataDir(candidates, exists) {
|
|
14831
14832
|
return candidates.find((c) => exists(c.dir)) ?? null;
|
|
14832
14833
|
}
|
|
14833
14834
|
function buzzAppCandidates(platform, env2, home) {
|
|
14834
14835
|
if (platform === "darwin") {
|
|
14835
|
-
return ["/Applications/Buzz.app",
|
|
14836
|
+
return ["/Applications/Buzz.app", join4(home, "Applications", "Buzz.app")];
|
|
14836
14837
|
}
|
|
14837
14838
|
if (platform === "win32") {
|
|
14838
|
-
const base = env2.LOCALAPPDATA ??
|
|
14839
|
-
return [
|
|
14839
|
+
const base = env2.LOCALAPPDATA ?? join4(home, "AppData", "Local");
|
|
14840
|
+
return [join4(base, "Programs", "Buzz")];
|
|
14840
14841
|
}
|
|
14841
14842
|
return [
|
|
14842
14843
|
"/opt/Buzz",
|
|
14843
14844
|
"/usr/bin/buzz-desktop",
|
|
14844
|
-
|
|
14845
|
+
join4(home, ".local", "bin", "buzz-desktop")
|
|
14845
14846
|
];
|
|
14846
14847
|
}
|
|
14847
14848
|
var BUZZ_RELEASES_URL = "https://github.com/block/buzz/releases/latest";
|
|
14848
14849
|
function buzzSidecarDirs(platform, env2, home) {
|
|
14849
14850
|
if (platform === "darwin") {
|
|
14850
|
-
return buzzAppCandidates(platform, env2, home).map((app) =>
|
|
14851
|
+
return buzzAppCandidates(platform, env2, home).map((app) => join4(app, "Contents", "MacOS"));
|
|
14851
14852
|
}
|
|
14852
14853
|
if (platform === "win32") {
|
|
14853
14854
|
return buzzAppCandidates(platform, env2, home);
|
|
@@ -14861,7 +14862,7 @@ function resolveBuzzBinary(bin, opts) {
|
|
|
14861
14862
|
if (bin.includes("/") || bin.includes("\\"))
|
|
14862
14863
|
return null;
|
|
14863
14864
|
for (const dir of buzzSidecarDirs(opts.platform, opts.env, opts.home)) {
|
|
14864
|
-
const candidate =
|
|
14865
|
+
const candidate = join4(dir, bin);
|
|
14865
14866
|
if (opts.isExecutable(candidate))
|
|
14866
14867
|
return { path: candidate, source: "desktop" };
|
|
14867
14868
|
}
|
|
@@ -14909,7 +14910,7 @@ function resolveOnPath(bin, env2, platform, isExecutable) {
|
|
|
14909
14910
|
const sep = platform === "win32" ? ";" : ":";
|
|
14910
14911
|
for (const dir of (env2.PATH ?? "").split(sep).filter(Boolean)) {
|
|
14911
14912
|
for (const ext of exts) {
|
|
14912
|
-
const candidate =
|
|
14913
|
+
const candidate = join4(dir, bin + ext);
|
|
14913
14914
|
if (isExecutable(candidate))
|
|
14914
14915
|
return candidate;
|
|
14915
14916
|
}
|
|
@@ -14931,7 +14932,7 @@ function whichBuzz(bin) {
|
|
|
14931
14932
|
return resolveBuzzBinary(bin, {
|
|
14932
14933
|
platform: process.platform,
|
|
14933
14934
|
env: process.env,
|
|
14934
|
-
home:
|
|
14935
|
+
home: homedir2(),
|
|
14935
14936
|
isExecutable: executableOnDisk
|
|
14936
14937
|
});
|
|
14937
14938
|
}
|
|
@@ -15247,14 +15248,14 @@ function registerChannelCommands(program2) {
|
|
|
15247
15248
|
console.log(json);
|
|
15248
15249
|
return;
|
|
15249
15250
|
}
|
|
15250
|
-
const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env,
|
|
15251
|
+
const chosen = opts.appData ? { build: "release", dir: String(opts.appData) } : chooseAppDataDir(buzzAppDataCandidates(process.platform, process.env, homedir2()), (d) => existsSync(d));
|
|
15251
15252
|
if (!chosen) {
|
|
15252
|
-
const candidates = buzzAppDataCandidates(process.platform, process.env,
|
|
15253
|
-
const appInstalled = buzzAppCandidates(process.platform, process.env,
|
|
15253
|
+
const candidates = buzzAppDataCandidates(process.platform, process.env, homedir2());
|
|
15254
|
+
const appInstalled = buzzAppCandidates(process.platform, process.env, homedir2()).some((p) => existsSync(p));
|
|
15254
15255
|
throw new CLIError(appInstalled ? "Buzz Desktop has no data directory yet." : "Buzz Desktop is not installed.", 3 /* NotFound */, missingAppDataMessage(candidates, appInstalled));
|
|
15255
15256
|
}
|
|
15256
|
-
const dir =
|
|
15257
|
-
const file =
|
|
15257
|
+
const dir = join4(chosen.dir, "custom_harnesses");
|
|
15258
|
+
const file = join4(dir, `${def.id}.json`);
|
|
15258
15259
|
if (existsSync(file) && !opts.force) {
|
|
15259
15260
|
throw new CLIError(`${file} already exists.`, 5 /* Conflict */, "Re-run with --force to overwrite it.");
|
|
15260
15261
|
}
|
|
@@ -15325,7 +15326,7 @@ function registerChannelCommands(program2) {
|
|
|
15325
15326
|
}
|
|
15326
15327
|
const acp = whichBuzz(opts.buzzAcpBin);
|
|
15327
15328
|
const childPath = [
|
|
15328
|
-
...buzzSidecarDirs(process.platform, process.env,
|
|
15329
|
+
...buzzSidecarDirs(process.platform, process.env, homedir2()).filter((d) => existsSync(d)),
|
|
15329
15330
|
process.env.PATH ?? ""
|
|
15330
15331
|
].filter(Boolean).join(process.platform === "win32" ? ";" : ":");
|
|
15331
15332
|
if (opts.dryRun) {
|
|
@@ -15766,6 +15767,7 @@ function registerConfigCommand(program2) {
|
|
|
15766
15767
|
}
|
|
15767
15768
|
|
|
15768
15769
|
// src/commands/definitions-apply.ts
|
|
15770
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
15769
15771
|
import fs8 from "node:fs";
|
|
15770
15772
|
import path8 from "node:path";
|
|
15771
15773
|
init_dist();
|
|
@@ -15987,16 +15989,19 @@ var VALID_KINDS = new Set([
|
|
|
15987
15989
|
"Function",
|
|
15988
15990
|
"Workflow",
|
|
15989
15991
|
"Agent",
|
|
15990
|
-
"Trigger"
|
|
15992
|
+
"Trigger",
|
|
15993
|
+
"ClientAccess"
|
|
15991
15994
|
]);
|
|
15992
15995
|
var KIND_TO_API_PATH = {
|
|
15993
15996
|
Function: "/api/admin/definitions/functions",
|
|
15994
15997
|
Workflow: "/api/admin/definitions/workflows",
|
|
15995
15998
|
Agent: "/api/admin/definitions/agents",
|
|
15996
|
-
Trigger: "/api/admin/triggers"
|
|
15999
|
+
Trigger: "/api/admin/triggers",
|
|
16000
|
+
ClientAccess: "/api/admin/definitions/client-access"
|
|
15997
16001
|
};
|
|
15998
16002
|
var KIND_TO_PLURAL = {
|
|
15999
16003
|
Function: "Functions",
|
|
16004
|
+
ClientAccess: "Client access",
|
|
16000
16005
|
Workflow: "Workflows",
|
|
16001
16006
|
Agent: "Agents",
|
|
16002
16007
|
Trigger: "Triggers"
|
|
@@ -16245,10 +16250,18 @@ function localSubsetOf(local, remote) {
|
|
|
16245
16250
|
if (typeof local === "object") {
|
|
16246
16251
|
const lObj = local;
|
|
16247
16252
|
const rObj = remote;
|
|
16248
|
-
return Object.keys(lObj).every((key) =>
|
|
16253
|
+
return Object.keys(lObj).every((key) => {
|
|
16254
|
+
if (key === "value" && typeof lObj.value === "string" && typeof rObj.value_sha256 === "string" && (rObj.value === null || rObj.value === undefined)) {
|
|
16255
|
+
return sha256Prefix(lObj.value) === rObj.value_sha256;
|
|
16256
|
+
}
|
|
16257
|
+
return localSubsetOf(lObj[key], rObj[key]);
|
|
16258
|
+
});
|
|
16249
16259
|
}
|
|
16250
16260
|
return false;
|
|
16251
16261
|
}
|
|
16262
|
+
function sha256Prefix(value) {
|
|
16263
|
+
return createHash3("sha256").update(value, "utf8").digest("hex").slice(0, 16);
|
|
16264
|
+
}
|
|
16252
16265
|
var SYMBOLS = {
|
|
16253
16266
|
create: "+",
|
|
16254
16267
|
update: "~",
|
|
@@ -16271,7 +16284,8 @@ function renderPlan(ops) {
|
|
|
16271
16284
|
"Function",
|
|
16272
16285
|
"Workflow",
|
|
16273
16286
|
"Agent",
|
|
16274
|
-
"Trigger"
|
|
16287
|
+
"Trigger",
|
|
16288
|
+
"ClientAccess"
|
|
16275
16289
|
]) {
|
|
16276
16290
|
const kindOps = grouped.get(kind);
|
|
16277
16291
|
if (!kindOps)
|
|
@@ -16320,6 +16334,7 @@ async function executeApply(api, ops, defs, warnings) {
|
|
|
16320
16334
|
defsByKey.set(`${d.kind}:${d.name}`, d);
|
|
16321
16335
|
}
|
|
16322
16336
|
const results = [];
|
|
16337
|
+
ops = [...ops].sort((a, b) => (a.kind === "ClientAccess" ? 1 : 0) - (b.kind === "ClientAccess" ? 1 : 0));
|
|
16323
16338
|
for (const op of ops) {
|
|
16324
16339
|
if (op.action === "skip") {
|
|
16325
16340
|
results.push({ name: op.name, kind: op.kind, action: "skipped" });
|
|
@@ -19237,11 +19252,11 @@ ${items.length} task(s) available`));
|
|
|
19237
19252
|
}
|
|
19238
19253
|
|
|
19239
19254
|
// src/commands/runner.ts
|
|
19240
|
-
import
|
|
19255
|
+
import os4 from "node:os";
|
|
19241
19256
|
init_errors();
|
|
19242
19257
|
function hostLabel() {
|
|
19243
19258
|
try {
|
|
19244
|
-
return
|
|
19259
|
+
return os4.hostname() || undefined;
|
|
19245
19260
|
} catch {
|
|
19246
19261
|
return;
|
|
19247
19262
|
}
|
|
@@ -21625,6 +21640,129 @@ ${models.length} models`));
|
|
|
21625
21640
|
});
|
|
21626
21641
|
}
|
|
21627
21642
|
|
|
21643
|
+
// src/commands/spend.ts
|
|
21644
|
+
init_errors();
|
|
21645
|
+
var GROUPINGS = ["day", "agent", "model", "surface", "billing_owner"];
|
|
21646
|
+
function usd(value) {
|
|
21647
|
+
return value == null ? "-" : `$${Number(value).toFixed(4)}`;
|
|
21648
|
+
}
|
|
21649
|
+
function registerSpendCommand(program2) {
|
|
21650
|
+
program2.command("spend").description("LLM spend for a tenant, from Martha's own ledger").option("--tenant <id>", "Tenant (defaults to your own; others need super-admin)").option("--group-by <dim>", `One of: ${GROUPINGS.join(", ")}`, "day").option("--since <YYYY-MM-DD>", "Inclusive UTC start day").option("--until <YYYY-MM-DD>", "Exclusive UTC end day").action(async (opts) => {
|
|
21651
|
+
const ctx = createContext({
|
|
21652
|
+
profileOverride: program2.opts().profile,
|
|
21653
|
+
verbose: program2.opts().verbose
|
|
21654
|
+
});
|
|
21655
|
+
if (program2.opts().apiUrl) {
|
|
21656
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
21657
|
+
}
|
|
21658
|
+
const isJson = !!program2.opts().json;
|
|
21659
|
+
if (!GROUPINGS.includes(opts.groupBy)) {
|
|
21660
|
+
throw new CLIError(`--group-by must be one of: ${GROUPINGS.join(", ")}`, 4 /* Validation */);
|
|
21661
|
+
}
|
|
21662
|
+
const tenant = opts.tenant ?? await ctx.getTenantId();
|
|
21663
|
+
const params = new URLSearchParams({ group_by: opts.groupBy });
|
|
21664
|
+
if (opts.since)
|
|
21665
|
+
params.set("since", opts.since);
|
|
21666
|
+
if (opts.until)
|
|
21667
|
+
params.set("until", opts.until);
|
|
21668
|
+
const data = await ctx.api.get(`/api/admin/tenants/${encodeURIComponent(tenant)}/spend?${params.toString()}`);
|
|
21669
|
+
if (isJson) {
|
|
21670
|
+
console.log(JSON.stringify(data, null, 2));
|
|
21671
|
+
return;
|
|
21672
|
+
}
|
|
21673
|
+
if (data.rows.length === 0) {
|
|
21674
|
+
console.log(source_default.dim(`No LLM spend recorded for ${data.tenant_id}.`));
|
|
21675
|
+
return;
|
|
21676
|
+
}
|
|
21677
|
+
const cols = [
|
|
21678
|
+
{ header: data.group_by.toUpperCase(), accessor: (r) => r.key ?? "-" },
|
|
21679
|
+
{ header: "CALLS", accessor: (r) => String(r.calls) },
|
|
21680
|
+
{ header: "PROMPT", accessor: (r) => String(r.prompt_tokens) },
|
|
21681
|
+
{ header: "COMPLETION", accessor: (r) => String(r.completion_tokens) },
|
|
21682
|
+
{ header: "CACHE READ", accessor: (r) => String(r.cache_read_tokens) },
|
|
21683
|
+
{ header: "CACHE WRITE", accessor: (r) => String(r.cache_write_tokens) },
|
|
21684
|
+
{ header: "COST", accessor: (r) => usd(r.cost_usd) },
|
|
21685
|
+
{ header: "UNPRICED", accessor: (r) => String(r.unpriced_calls) }
|
|
21686
|
+
];
|
|
21687
|
+
const widths = cols.map((col) => Math.max(col.header.length, ...data.rows.map((r) => col.accessor(r).length)));
|
|
21688
|
+
const header = cols.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
21689
|
+
console.log(source_default.bold(header));
|
|
21690
|
+
console.log(source_default.dim("-".repeat(header.length)));
|
|
21691
|
+
for (const row of data.rows) {
|
|
21692
|
+
console.log(cols.map((col, i) => col.accessor(row).padEnd(widths[i])).join(" "));
|
|
21693
|
+
}
|
|
21694
|
+
const t = data.totals;
|
|
21695
|
+
console.log(source_default.dim(`
|
|
21696
|
+
${data.tenant_id}: ${t.calls} calls, ${usd(t.cost_usd)}` + (t.unpriced_calls ? `, ${t.unpriced_calls} unpriced (not included in cost)` : "")));
|
|
21697
|
+
});
|
|
21698
|
+
}
|
|
21699
|
+
|
|
21700
|
+
// src/commands/budget.ts
|
|
21701
|
+
init_errors();
|
|
21702
|
+
var PERIODS = ["day", "month"];
|
|
21703
|
+
var ACTIONS = ["warn", "require_approval", "block"];
|
|
21704
|
+
function oneOf(value, allowed, flag) {
|
|
21705
|
+
if (!allowed.includes(value)) {
|
|
21706
|
+
throw new CLIError(`${flag} must be one of: ${allowed.join(", ")}`, 4 /* Validation */);
|
|
21707
|
+
}
|
|
21708
|
+
}
|
|
21709
|
+
function registerBudgetCommands(program2) {
|
|
21710
|
+
const budget = program2.command("budget").description("LLM spend ceilings for a tenant");
|
|
21711
|
+
const context = () => {
|
|
21712
|
+
const ctx = createContext({
|
|
21713
|
+
profileOverride: program2.opts().profile,
|
|
21714
|
+
verbose: program2.opts().verbose
|
|
21715
|
+
});
|
|
21716
|
+
if (program2.opts().apiUrl)
|
|
21717
|
+
ctx.profile.api_url = program2.opts().apiUrl;
|
|
21718
|
+
return ctx;
|
|
21719
|
+
};
|
|
21720
|
+
const path9 = async (ctx, tenant) => `/api/admin/tenants/${encodeURIComponent(tenant ?? await ctx.getTenantId())}/llm-budgets`;
|
|
21721
|
+
budget.command("list").description("List the tenant's ceilings").option("--tenant <id>", "Tenant (defaults to your own)").action(async (opts) => {
|
|
21722
|
+
const ctx = context();
|
|
21723
|
+
const data = await ctx.api.get(await path9(ctx, opts.tenant));
|
|
21724
|
+
if (program2.opts().json) {
|
|
21725
|
+
console.log(JSON.stringify(data, null, 2));
|
|
21726
|
+
return;
|
|
21727
|
+
}
|
|
21728
|
+
if (data.budgets.length === 0) {
|
|
21729
|
+
console.log(source_default.dim(`No LLM spend ceilings for ${data.tenant_id}.`));
|
|
21730
|
+
return;
|
|
21731
|
+
}
|
|
21732
|
+
for (const b of data.budgets) {
|
|
21733
|
+
const scope = b.agent_id ? `agent ${b.agent_id}` : "tenant";
|
|
21734
|
+
console.log(`${b.id} ${scope.padEnd(42)} ${b.period.padEnd(6)} $${b.limit_usd} ${b.action}`);
|
|
21735
|
+
}
|
|
21736
|
+
});
|
|
21737
|
+
budget.command("set").description("Create or replace a ceiling (one per tenant-or-agent + period)").requiredOption("--period <p>", `One of: ${PERIODS.join(", ")}`).requiredOption("--limit <usd>", "Ceiling in USD (cache reads included)").requiredOption("--action <a>", `One of: ${ACTIONS.join(", ")}`).option("--agent <uuid>", "Scope to one AI Agent (default: whole tenant)").option("--note <text>", "Why this ceiling exists").option("--tenant <id>", "Tenant (defaults to your own)").action(async (opts) => {
|
|
21738
|
+
oneOf(opts.period, PERIODS, "--period");
|
|
21739
|
+
oneOf(opts.action, ACTIONS, "--action");
|
|
21740
|
+
if (!/^\d+(\.\d{1,4})?$/.test(opts.limit)) {
|
|
21741
|
+
throw new CLIError("--limit must be a non-negative USD amount", 4 /* Validation */);
|
|
21742
|
+
}
|
|
21743
|
+
const ctx = context();
|
|
21744
|
+
const saved = await ctx.api.put(await path9(ctx, opts.tenant), {
|
|
21745
|
+
period: opts.period,
|
|
21746
|
+
limit_usd: opts.limit,
|
|
21747
|
+
action: opts.action,
|
|
21748
|
+
agent_id: opts.agent ?? null,
|
|
21749
|
+
note: opts.note ?? null
|
|
21750
|
+
});
|
|
21751
|
+
if (program2.opts().json)
|
|
21752
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
21753
|
+
else
|
|
21754
|
+
console.log(`${saved.id} ${saved.period} $${saved.limit_usd} -> ${saved.action}`);
|
|
21755
|
+
});
|
|
21756
|
+
budget.command("rm <budgetId>").description("Delete a ceiling").option("--tenant <id>", "Tenant (defaults to your own)").action(async (budgetId, opts) => {
|
|
21757
|
+
const ctx = context();
|
|
21758
|
+
await ctx.api.del(`${await path9(ctx, opts.tenant)}/${encodeURIComponent(budgetId)}`);
|
|
21759
|
+
if (program2.opts().json)
|
|
21760
|
+
console.log(JSON.stringify({ deleted: budgetId }));
|
|
21761
|
+
else
|
|
21762
|
+
console.log(`deleted ${budgetId}`);
|
|
21763
|
+
});
|
|
21764
|
+
}
|
|
21765
|
+
|
|
21628
21766
|
// src/commands/wiki.ts
|
|
21629
21767
|
import fs11 from "node:fs";
|
|
21630
21768
|
init_errors();
|
|
@@ -22480,7 +22618,7 @@ async function resolveCredentialFlag(value) {
|
|
|
22480
22618
|
|
|
22481
22619
|
// src/commands/policy.ts
|
|
22482
22620
|
init_errors();
|
|
22483
|
-
var
|
|
22621
|
+
var ACTIONS2 = ["allow", "require_approval", "block"];
|
|
22484
22622
|
var KINDS2 = ["risk_tag", "risk_level", "capability"];
|
|
22485
22623
|
var SCOPES = ["any", "agent", "client"];
|
|
22486
22624
|
var truncate4 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
@@ -22550,8 +22688,8 @@ function registerPolicyCommands(program2) {
|
|
|
22550
22688
|
console.log(columns.map((c, i) => c.get(r).padEnd(widths[i])).join(" "));
|
|
22551
22689
|
}
|
|
22552
22690
|
});
|
|
22553
|
-
cmd.command("set-default <action>").description(`Set the opt-in default action (${
|
|
22554
|
-
assertOneOf(action,
|
|
22691
|
+
cmd.command("set-default <action>").description(`Set the opt-in default action (${ACTIONS2.join("|")})`).action(async (action) => {
|
|
22692
|
+
assertOneOf(action, ACTIONS2, "action");
|
|
22555
22693
|
const ctx = getCtx();
|
|
22556
22694
|
const res = await ctx.api.put("/api/admin/policy/settings", { default_action: action });
|
|
22557
22695
|
if (isJson()) {
|
|
@@ -22575,9 +22713,9 @@ function registerPolicyCommands(program2) {
|
|
|
22575
22713
|
console.log(`Safe-default bundle ${enabled ? source_default.yellow("enabled") : source_default.dim("disabled")}.`);
|
|
22576
22714
|
});
|
|
22577
22715
|
const rule = cmd.command("rule").description("Manage explicit policy rules");
|
|
22578
|
-
rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS2.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${
|
|
22716
|
+
rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS2.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS2.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
|
|
22579
22717
|
assertOneOf(opts.kind, KINDS2, "--kind");
|
|
22580
|
-
assertOneOf(opts.action,
|
|
22718
|
+
assertOneOf(opts.action, ACTIONS2, "--action");
|
|
22581
22719
|
assertOneOf(opts.scope, SCOPES, "--scope");
|
|
22582
22720
|
const ctx = getCtx();
|
|
22583
22721
|
const res = await ctx.api.post("/api/admin/policy/rules", {
|
|
@@ -22608,6 +22746,7 @@ function registerPolicyCommands(program2) {
|
|
|
22608
22746
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
22609
22747
|
init_errors();
|
|
22610
22748
|
var BASE = "/api/admin/agent-step/trust-roots";
|
|
22749
|
+
var FLEET_BASE = "/api/admin/agent-step/fleet";
|
|
22611
22750
|
function resolveKeyMaterial(value) {
|
|
22612
22751
|
if (value.startsWith("@")) {
|
|
22613
22752
|
const path9 = value.slice(1);
|
|
@@ -22721,6 +22860,69 @@ function registerAgentStepCommands(program2) {
|
|
|
22721
22860
|
console.log(source_default.green(`Rotated trust root '${tr.name}'`));
|
|
22722
22861
|
printOne(tr);
|
|
22723
22862
|
});
|
|
22863
|
+
const fleet = cmd.command("fleet").description("Pause / resume this tenant's agent_step fleet (#1041)");
|
|
22864
|
+
function printFleet(s) {
|
|
22865
|
+
const state = s.effective_paused ? source_default.red(`PAUSED (${s.effective_scope})`) : source_default.green("running");
|
|
22866
|
+
console.log(`agent_step fleet: ${source_default.bold(state)}`);
|
|
22867
|
+
if (s.effective_reason)
|
|
22868
|
+
console.log(` Reason: ${s.effective_reason}`);
|
|
22869
|
+
if (s.paused_by)
|
|
22870
|
+
console.log(` Paused by: ${s.paused_by}`);
|
|
22871
|
+
if (s.paused_at)
|
|
22872
|
+
console.log(` Paused at: ${s.paused_at}`);
|
|
22873
|
+
if (s.paused_workflows.length) {
|
|
22874
|
+
console.log(` Quarantined workflows: ${s.paused_workflows.join(", ")}`);
|
|
22875
|
+
}
|
|
22876
|
+
if (s.global_paused) {
|
|
22877
|
+
console.log(source_default.yellow(` NOTE: the PLATFORM-wide break-glass flag is set — every tenant is halted,
|
|
22878
|
+
` + " and resuming this tenant will not restart work until it is cleared."));
|
|
22879
|
+
}
|
|
22880
|
+
}
|
|
22881
|
+
fleet.command("status").description("Show whether agent_step work is currently halted").action(async () => {
|
|
22882
|
+
const ctx = getCtx();
|
|
22883
|
+
const s = await ctx.api.get(FLEET_BASE);
|
|
22884
|
+
if (isJson()) {
|
|
22885
|
+
console.log(JSON.stringify(s, null, 2));
|
|
22886
|
+
return;
|
|
22887
|
+
}
|
|
22888
|
+
printFleet(s);
|
|
22889
|
+
});
|
|
22890
|
+
fleet.command("pause").description("Halt all agent_step runs for this tenant (in-flight steps drain)").requiredOption("-r, --reason <text>", "Why you are stopping the fleet — recorded in the audit log and shown on stopped runs").action(async (opts) => {
|
|
22891
|
+
const ctx = getCtx();
|
|
22892
|
+
const s = await ctx.api.put(FLEET_BASE, {
|
|
22893
|
+
paused: true,
|
|
22894
|
+
reason: opts.reason
|
|
22895
|
+
});
|
|
22896
|
+
if (isJson()) {
|
|
22897
|
+
console.log(JSON.stringify(s, null, 2));
|
|
22898
|
+
return;
|
|
22899
|
+
}
|
|
22900
|
+
console.log(source_default.yellow(`Fleet paused. No new agent_step will start; in-flight steps finish their
|
|
22901
|
+
` + "current attempt and then stop instead of retrying."));
|
|
22902
|
+
printFleet(s);
|
|
22903
|
+
});
|
|
22904
|
+
fleet.command("resume").description("Allow agent_step runs for this tenant again").action(async () => {
|
|
22905
|
+
const ctx = getCtx();
|
|
22906
|
+
const s = await ctx.api.put(FLEET_BASE, { paused: false });
|
|
22907
|
+
if (isJson()) {
|
|
22908
|
+
console.log(JSON.stringify(s, null, 2));
|
|
22909
|
+
return;
|
|
22910
|
+
}
|
|
22911
|
+
console.log(source_default.green("Fleet resumed."));
|
|
22912
|
+
printFleet(s);
|
|
22913
|
+
});
|
|
22914
|
+
fleet.command("quarantine [workflows...]").description("Halt only the named workflows (pass none to clear the quarantine list)").action(async (workflows) => {
|
|
22915
|
+
const ctx = getCtx();
|
|
22916
|
+
const s = await ctx.api.put(FLEET_BASE, {
|
|
22917
|
+
paused_workflows: workflows ?? []
|
|
22918
|
+
});
|
|
22919
|
+
if (isJson()) {
|
|
22920
|
+
console.log(JSON.stringify(s, null, 2));
|
|
22921
|
+
return;
|
|
22922
|
+
}
|
|
22923
|
+
console.log(workflows?.length ? source_default.yellow(`Quarantined: ${workflows.join(", ")}`) : source_default.green("Quarantine cleared."));
|
|
22924
|
+
printFleet(s);
|
|
22925
|
+
});
|
|
22724
22926
|
}
|
|
22725
22927
|
|
|
22726
22928
|
// src/commands/authz.ts
|
|
@@ -24141,6 +24343,8 @@ registerNotificationCommands(program2);
|
|
|
24141
24343
|
registerMessagingCommands(program2);
|
|
24142
24344
|
registerClientCommands(program2);
|
|
24143
24345
|
registerModelsCommand(program2);
|
|
24346
|
+
registerSpendCommand(program2);
|
|
24347
|
+
registerBudgetCommands(program2);
|
|
24144
24348
|
registerSessionCommands(program2);
|
|
24145
24349
|
registerMCPCommands(program2);
|
|
24146
24350
|
registerPolicyCommands(program2);
|