@paybond/kit 0.9.7 → 0.10.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/dist/cli/audit-export.d.ts +7 -0
- package/dist/cli/audit-export.js +120 -0
- package/dist/cli/automation.d.ts +25 -0
- package/dist/cli/automation.js +297 -0
- package/dist/cli/body.d.ts +7 -0
- package/dist/cli/body.js +22 -0
- package/dist/cli/color.d.ts +15 -0
- package/dist/cli/color.js +39 -0
- package/dist/cli/command-spec.d.ts +12 -0
- package/dist/cli/command-spec.js +330 -0
- package/dist/cli/commands/discovery.d.ts +8 -0
- package/dist/cli/commands/discovery.js +194 -0
- package/dist/cli/commands/setup.d.ts +15 -0
- package/dist/cli/commands/setup.js +397 -0
- package/dist/cli/commands/workflows.d.ts +7 -0
- package/dist/cli/commands/workflows.js +209 -0
- package/dist/cli/config.d.ts +14 -0
- package/dist/cli/config.js +96 -0
- package/dist/cli/context.d.ts +22 -0
- package/dist/cli/context.js +109 -0
- package/dist/cli/credentials.d.ts +21 -0
- package/dist/cli/credentials.js +141 -0
- package/dist/cli/doctor-agent.d.ts +15 -0
- package/dist/cli/doctor-agent.js +311 -0
- package/dist/cli/envelope.d.ts +14 -0
- package/dist/cli/envelope.js +46 -0
- package/dist/cli/globals.d.ts +22 -0
- package/dist/cli/globals.js +238 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.js +51 -0
- package/dist/cli/mcp-install.d.ts +41 -0
- package/dist/cli/mcp-install.js +95 -0
- package/dist/cli/mcp-policy.d.ts +23 -0
- package/dist/cli/mcp-policy.js +104 -0
- package/dist/cli/mcp-verify-config.d.ts +37 -0
- package/dist/cli/mcp-verify-config.js +174 -0
- package/dist/cli/redact.d.ts +4 -0
- package/dist/cli/redact.js +67 -0
- package/dist/cli/request-id.d.ts +2 -0
- package/dist/cli/request-id.js +5 -0
- package/dist/cli/router.d.ts +2 -0
- package/dist/cli/router.js +275 -0
- package/dist/cli/suggest.d.ts +4 -0
- package/dist/cli/suggest.js +58 -0
- package/dist/cli/support-diagnostics.d.ts +19 -0
- package/dist/cli/support-diagnostics.js +47 -0
- package/dist/cli/types.d.ts +72 -0
- package/dist/cli/types.js +60 -0
- package/dist/cli/ux.d.ts +8 -0
- package/dist/cli/ux.js +164 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +38 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +136 -47
- package/dist/init.js +9 -14
- package/dist/login.d.ts +14 -1
- package/dist/login.js +123 -63
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +204 -76
- package/package.json +5 -2
package/dist/cli/ux.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { COMMAND_EXAMPLES, COMPLETION_SCRIPTS, WORKFLOWS } from "./command-spec.js";
|
|
4
|
+
import { resolvedDefaultsForDoctor } from "./credentials.js";
|
|
5
|
+
import { helpForCommand } from "./help.js";
|
|
6
|
+
import { consumeFlag } from "./globals.js";
|
|
7
|
+
import { planMcpInstall, parseMcpInstallFormat, parseMcpInstallHost, parseMcpInstallScope } from "./mcp-install.js";
|
|
8
|
+
import { CliError } from "./types.js";
|
|
9
|
+
import { handleDoctor } from "./commands/setup.js";
|
|
10
|
+
export function resolveHelpPath(argv) {
|
|
11
|
+
return argv.filter((part) => part !== "--help" && part !== "-h").join(" ");
|
|
12
|
+
}
|
|
13
|
+
export function renderHelpText(path) {
|
|
14
|
+
return helpForCommand(path);
|
|
15
|
+
}
|
|
16
|
+
export function handleHelpCommand(argv) {
|
|
17
|
+
const helpPath = resolveHelpPath(argv);
|
|
18
|
+
return { data: { text: renderHelpText(helpPath), path: helpPath || "paybond" } };
|
|
19
|
+
}
|
|
20
|
+
export function handleExamplesCommand(argv) {
|
|
21
|
+
const filterPath = argv.filter((part) => part !== "--help" && part !== "-h").join(" ");
|
|
22
|
+
const lines = [];
|
|
23
|
+
if (!filterPath) {
|
|
24
|
+
lines.push("Workflows:");
|
|
25
|
+
for (const workflow of WORKFLOWS) {
|
|
26
|
+
lines.push("", workflow.title);
|
|
27
|
+
if (workflow.description) {
|
|
28
|
+
lines.push(workflow.description);
|
|
29
|
+
}
|
|
30
|
+
for (const example of workflow.examples) {
|
|
31
|
+
lines.push(` $ ${example}`);
|
|
32
|
+
}
|
|
33
|
+
if (workflow.next) {
|
|
34
|
+
lines.push(` Next: ${workflow.next}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lines.push("", "Commands:");
|
|
38
|
+
}
|
|
39
|
+
const entries = filterPath
|
|
40
|
+
? Object.entries(COMMAND_EXAMPLES).filter(([commandPath]) => commandPath === filterPath || commandPath.startsWith(`${filterPath} `))
|
|
41
|
+
: Object.entries(COMMAND_EXAMPLES);
|
|
42
|
+
if (filterPath && entries.length === 0) {
|
|
43
|
+
throw new CliError(`no examples found for: ${filterPath}`, {
|
|
44
|
+
category: "usage",
|
|
45
|
+
code: "cli.usage.unknown_command",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const [commandPath, examples] of entries) {
|
|
49
|
+
lines.push("", `paybond ${commandPath}`);
|
|
50
|
+
for (const example of examples) {
|
|
51
|
+
lines.push(` $ ${example}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { data: { text: lines.join("\n").trim(), filter: filterPath || null, count: entries.length } };
|
|
55
|
+
}
|
|
56
|
+
export function handleCompletionCommand(argv) {
|
|
57
|
+
const shell = argv[0];
|
|
58
|
+
if (!shell || shell === "--help" || shell === "-h") {
|
|
59
|
+
throw new CliError("completion requires bash|zsh|fish", {
|
|
60
|
+
category: "usage",
|
|
61
|
+
code: "cli.usage.missing_completion_shell",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const script = COMPLETION_SCRIPTS[shell];
|
|
65
|
+
if (!script) {
|
|
66
|
+
throw new CliError(`unsupported completion shell: ${shell} (expected bash|zsh|fish)`, {
|
|
67
|
+
category: "usage",
|
|
68
|
+
code: "cli.usage.invalid_completion_shell",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { data: { shell, script } };
|
|
72
|
+
}
|
|
73
|
+
export async function handleOnboarding(ctx, argv) {
|
|
74
|
+
const hostFlag = consumeFlag(argv, "--host");
|
|
75
|
+
let installHost = "generic";
|
|
76
|
+
try {
|
|
77
|
+
if (hostFlag.present) {
|
|
78
|
+
installHost = parseMcpInstallHost(hostFlag.value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
throw new CliError(err instanceof Error ? err.message : String(err), {
|
|
83
|
+
category: "usage",
|
|
84
|
+
code: "cli.usage.invalid_mcp_install",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const steps = [];
|
|
88
|
+
steps.push({
|
|
89
|
+
name: "runtime",
|
|
90
|
+
ok: true,
|
|
91
|
+
message: `node ${process.version}`,
|
|
92
|
+
});
|
|
93
|
+
const defaults = resolvedDefaultsForDoctor(ctx.globals);
|
|
94
|
+
const envPath = path.isAbsolute(defaults.envFile)
|
|
95
|
+
? path.resolve(defaults.envFile)
|
|
96
|
+
: path.resolve(ctx.cwd, defaults.envFile);
|
|
97
|
+
let loggedIn = false;
|
|
98
|
+
try {
|
|
99
|
+
const { resolveApiKey } = await import("./credentials.js");
|
|
100
|
+
const apiKey = await resolveApiKey(ctx.globals, ctx.cwd);
|
|
101
|
+
loggedIn = true;
|
|
102
|
+
const { maskApiKey } = await import("./redact.js");
|
|
103
|
+
steps.push({
|
|
104
|
+
name: "login",
|
|
105
|
+
ok: true,
|
|
106
|
+
message: `credentials found (${maskApiKey(apiKey)})`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
steps.push({
|
|
111
|
+
name: "login",
|
|
112
|
+
ok: false,
|
|
113
|
+
message: err instanceof Error ? err.message : String(err),
|
|
114
|
+
command: "paybond login",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const guardrailPath = path.resolve(ctx.cwd, "paybond-paid-tool-guard.ts");
|
|
118
|
+
let guardrailExists = false;
|
|
119
|
+
try {
|
|
120
|
+
await access(guardrailPath);
|
|
121
|
+
guardrailExists = true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
guardrailExists = false;
|
|
125
|
+
}
|
|
126
|
+
steps.push({
|
|
127
|
+
name: "guardrail",
|
|
128
|
+
ok: guardrailExists,
|
|
129
|
+
message: guardrailExists ? `found ${guardrailPath}` : `guardrail file not found (${guardrailPath})`,
|
|
130
|
+
command: guardrailExists ? undefined : "paybond init guardrail",
|
|
131
|
+
});
|
|
132
|
+
const plan = planMcpInstall({
|
|
133
|
+
host: installHost,
|
|
134
|
+
scope: parseMcpInstallScope("local"),
|
|
135
|
+
format: parseMcpInstallFormat(undefined, installHost),
|
|
136
|
+
envFile: defaults.envFile,
|
|
137
|
+
cwd: ctx.cwd,
|
|
138
|
+
home: process.env.HOME ?? process.env.USERPROFILE ?? ctx.cwd,
|
|
139
|
+
});
|
|
140
|
+
steps.push({
|
|
141
|
+
name: "mcp_config",
|
|
142
|
+
ok: true,
|
|
143
|
+
message: `preview ready for host=${plan.host} (non-destructive --scope local)`,
|
|
144
|
+
command: `paybond mcp install --host ${plan.host} --scope local`,
|
|
145
|
+
});
|
|
146
|
+
const doctor = await handleDoctor(ctx, loggedIn ? ["--agent"] : []);
|
|
147
|
+
const doctorOk = String(doctor.data.summary) === "pass";
|
|
148
|
+
steps.push({
|
|
149
|
+
name: "doctor",
|
|
150
|
+
ok: doctorOk,
|
|
151
|
+
message: `doctor ${doctor.data.summary}`,
|
|
152
|
+
command: doctorOk ? undefined : "paybond doctor --agent",
|
|
153
|
+
});
|
|
154
|
+
const summary = steps.every((step) => step.ok) ? "pass" : "fail";
|
|
155
|
+
return {
|
|
156
|
+
data: {
|
|
157
|
+
steps,
|
|
158
|
+
summary,
|
|
159
|
+
env_file: envPath,
|
|
160
|
+
mcp_preview: plan.printed ? plan.payload : undefined,
|
|
161
|
+
doctor_checks: doctor.data.checks,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { runCli } from "./cli/router.js";
|
|
6
|
+
async function invokedFromCLI() {
|
|
7
|
+
const scriptPath = process.argv[1];
|
|
8
|
+
if (!scriptPath) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
async function realFileURL(filePath) {
|
|
12
|
+
let resolved = path.resolve(filePath);
|
|
13
|
+
try {
|
|
14
|
+
resolved = await fs.realpath(resolved);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// keep absolute path
|
|
18
|
+
}
|
|
19
|
+
const href = pathToFileURL(resolved).href;
|
|
20
|
+
return href.startsWith("file:///var/") ? href.replace("file:///var/", "file:///private/var/") : href;
|
|
21
|
+
}
|
|
22
|
+
return (await realFileURL(scriptPath)) === (await realFileURL(fileURLToPath(import.meta.url)));
|
|
23
|
+
}
|
|
24
|
+
invokedFromCLI().then((invoked) => {
|
|
25
|
+
if (!invoked) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
runCli(process.argv.slice(2)).then((code) => {
|
|
29
|
+
process.exitCode = code;
|
|
30
|
+
}, (err) => {
|
|
31
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
});
|
|
34
|
+
}, (err) => {
|
|
35
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
});
|
|
38
|
+
export { runCli } from "./cli/router.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type BuildSignedCreateIntentParams, type SettlementRail } from "./principal-intent.js";
|
|
6
6
|
import { type SignPayeeEvidenceParams } from "./payee-evidence.js";
|
|
7
|
+
export type SpendScope = {
|
|
8
|
+
scope_type: string;
|
|
9
|
+
scope_key: string;
|
|
10
|
+
};
|
|
7
11
|
export type VerifyCapabilityResult = {
|
|
8
12
|
allow: boolean;
|
|
9
13
|
auditId: string;
|
|
@@ -11,6 +15,28 @@ export type VerifyCapabilityResult = {
|
|
|
11
15
|
intentId: string;
|
|
12
16
|
code?: string;
|
|
13
17
|
message?: string;
|
|
18
|
+
decisionId?: string;
|
|
19
|
+
approvalRequestId?: string;
|
|
20
|
+
policyVersion?: number;
|
|
21
|
+
reasonCodes?: string[];
|
|
22
|
+
spendScope?: SpendScope;
|
|
23
|
+
remainingCents?: number;
|
|
24
|
+
retryAfter?: number;
|
|
25
|
+
/** True when gateway policy requires operator approval before execution may proceed. */
|
|
26
|
+
approvalRequired?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type PaybondSpendAuthorizationInput = {
|
|
29
|
+
operation: string;
|
|
30
|
+
requestedSpendCents?: number;
|
|
31
|
+
vendorId?: string;
|
|
32
|
+
taskId?: string;
|
|
33
|
+
workflowId?: string;
|
|
34
|
+
toolCallId?: string;
|
|
35
|
+
toolName?: string;
|
|
36
|
+
currency?: string;
|
|
37
|
+
agentSubject?: string;
|
|
38
|
+
approvalToken?: string;
|
|
39
|
+
idempotencyKey?: string;
|
|
14
40
|
};
|
|
15
41
|
export type SubmitEvidenceResult = {
|
|
16
42
|
intentId: string;
|
|
@@ -715,14 +741,19 @@ export declare class PaybondCapabilityBinding {
|
|
|
715
741
|
authorizeSpend(input: PaybondSpendAuthorizationInput): Promise<VerifyCapabilityResult>;
|
|
716
742
|
}
|
|
717
743
|
export type PaybondSpendGuardInit = {
|
|
718
|
-
harbor: Pick<HarborClient | GatewayHarborClient, "tenantId" | "verifyCapability"
|
|
744
|
+
harbor: Pick<HarborClient | GatewayHarborClient, "tenantId" | "verifyCapability"> & {
|
|
745
|
+
completeSpendDecision?: (input: {
|
|
746
|
+
decisionId: string;
|
|
747
|
+
outcome: "consumed" | "released";
|
|
748
|
+
}) => Promise<void>;
|
|
749
|
+
};
|
|
719
750
|
intentId: string;
|
|
720
751
|
capabilityToken: string;
|
|
721
752
|
};
|
|
722
|
-
export
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
753
|
+
export declare class PaybondSpendApprovalRequiredError extends Error {
|
|
754
|
+
readonly result: VerifyCapabilityResult;
|
|
755
|
+
constructor(result: VerifyCapabilityResult);
|
|
756
|
+
}
|
|
726
757
|
export declare class PaybondSpendDeniedError extends Error {
|
|
727
758
|
readonly result: VerifyCapabilityResult;
|
|
728
759
|
constructor(result: VerifyCapabilityResult);
|
|
@@ -730,13 +761,15 @@ export declare class PaybondSpendDeniedError extends Error {
|
|
|
730
761
|
export type PaybondToolHandler<TArgs extends unknown[], TResult> = (...args: TArgs) => TResult | Promise<TResult>;
|
|
731
762
|
export type PaybondGuardedToolHandler<TArgs extends unknown[], TResult> = (...args: TArgs) => Promise<Awaited<TResult>>;
|
|
732
763
|
export declare class PaybondSpendGuard {
|
|
733
|
-
readonly harbor:
|
|
764
|
+
readonly harbor: PaybondSpendGuardInit["harbor"];
|
|
734
765
|
readonly intentId: string;
|
|
735
766
|
readonly capabilityToken: string;
|
|
736
767
|
constructor(init: PaybondSpendGuardInit | PaybondCapabilityBinding);
|
|
737
768
|
verifySpendCapability(input: PaybondSpendAuthorizationInput): Promise<VerifyCapabilityResult>;
|
|
738
769
|
authorizeSpend(input: PaybondSpendAuthorizationInput): Promise<VerifyCapabilityResult>;
|
|
739
770
|
assertSpendAuthorized(input: PaybondSpendAuthorizationInput): Promise<VerifyCapabilityResult>;
|
|
771
|
+
/** Finalizes scope reservations tied to an authorization decision after tool execution. */
|
|
772
|
+
completeSpendAuthorization(decisionId: string, outcome: "consumed" | "released"): Promise<void>;
|
|
740
773
|
guardTool<TArgs extends unknown[], TResult>(input: PaybondSpendAuthorizationInput, handler: PaybondToolHandler<TArgs, TResult>): PaybondGuardedToolHandler<TArgs, TResult>;
|
|
741
774
|
}
|
|
742
775
|
export declare function authorizeSpend(source: PaybondSpendGuardInit | PaybondCapabilityBinding, input: PaybondSpendAuthorizationInput): Promise<VerifyCapabilityResult>;
|
|
@@ -799,11 +832,9 @@ export declare class HarborClient {
|
|
|
799
832
|
* @throws HarborHttpError when HTTP fails
|
|
800
833
|
* @throws Error when Harbor echoes a different tenant / intent than requested
|
|
801
834
|
*/
|
|
802
|
-
verifyCapability(input: {
|
|
835
|
+
verifyCapability(input: PaybondSpendAuthorizationInput & {
|
|
803
836
|
intentId: string;
|
|
804
837
|
token: string;
|
|
805
|
-
operation: string;
|
|
806
|
-
requestedSpendCents?: number;
|
|
807
838
|
}): Promise<VerifyCapabilityResult>;
|
|
808
839
|
verifySpendCapability(input: {
|
|
809
840
|
intentId: string;
|
|
@@ -897,11 +928,9 @@ export declare class GatewayHarborClient {
|
|
|
897
928
|
private fetchWithRetries;
|
|
898
929
|
private postJSON;
|
|
899
930
|
private mutationHeaders;
|
|
900
|
-
verifyCapability(input: {
|
|
931
|
+
verifyCapability(input: PaybondSpendAuthorizationInput & {
|
|
901
932
|
intentId: string;
|
|
902
933
|
token: string;
|
|
903
|
-
operation: string;
|
|
904
|
-
requestedSpendCents?: number;
|
|
905
934
|
}): Promise<VerifyCapabilityResult>;
|
|
906
935
|
verifySpendCapability(input: {
|
|
907
936
|
intentId: string;
|
|
@@ -915,6 +944,11 @@ export declare class GatewayHarborClient {
|
|
|
915
944
|
operation: string;
|
|
916
945
|
requestedSpendCents?: number;
|
|
917
946
|
}): Promise<VerifyCapabilityResult>;
|
|
947
|
+
/** Finalizes active spend reservations after tool execution completes or is aborted. */
|
|
948
|
+
completeSpendDecision(input: {
|
|
949
|
+
decisionId: string;
|
|
950
|
+
outcome: "consumed" | "released";
|
|
951
|
+
}): Promise<void>;
|
|
918
952
|
createIntent(body: Record<string, unknown>, options: GatewayHarborMutationOptions): Promise<Record<string, unknown>>;
|
|
919
953
|
fundIntent(intentId: string, options: GatewayHarborMutationOptions & {
|
|
920
954
|
paymentSignature?: string;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,77 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { buildSignedCreateIntentBody, } from "./principal-intent.js";
|
|
6
6
|
import { signPayeeEvidenceBinding } from "./payee-evidence.js";
|
|
7
|
+
function parseVerifyCapabilityBody(body, expectedTenant, expectedIntentId) {
|
|
8
|
+
const tenant = String(body.tenant ?? "");
|
|
9
|
+
const intentId = String(body.intent_id ?? "");
|
|
10
|
+
if (tenant !== expectedTenant) {
|
|
11
|
+
throw new Error(`verify tenant mismatch: client=${expectedTenant} remote=${tenant}`);
|
|
12
|
+
}
|
|
13
|
+
if (intentId !== expectedIntentId) {
|
|
14
|
+
throw new Error(`verify intent mismatch: requested=${expectedIntentId} remote=${intentId}`);
|
|
15
|
+
}
|
|
16
|
+
const reasonCodes = Array.isArray(body.reason_codes)
|
|
17
|
+
? body.reason_codes.map((value) => String(value))
|
|
18
|
+
: undefined;
|
|
19
|
+
const spendScope = body.spend_scope && typeof body.spend_scope === "object" && !Array.isArray(body.spend_scope)
|
|
20
|
+
? {
|
|
21
|
+
scope_type: String(body.spend_scope.scope_type ?? ""),
|
|
22
|
+
scope_key: String(body.spend_scope.scope_key ?? ""),
|
|
23
|
+
}
|
|
24
|
+
: undefined;
|
|
25
|
+
const code = body.code != null ? String(body.code) : undefined;
|
|
26
|
+
const approvalRequired = code === "approval_required" ||
|
|
27
|
+
reasonCodes?.includes("approval_threshold_exceeded") ||
|
|
28
|
+
reasonCodes?.includes("approval_required_pending") ||
|
|
29
|
+
reasonCodes?.includes("anomaly_new_vendor") ||
|
|
30
|
+
reasonCodes?.includes("anomaly_amount_spike") ||
|
|
31
|
+
reasonCodes?.includes("anomaly_rapid_auth") ||
|
|
32
|
+
reasonCodes?.includes("anomaly_cap_proximity") ||
|
|
33
|
+
false;
|
|
34
|
+
return {
|
|
35
|
+
allow: Boolean(body.allow),
|
|
36
|
+
auditId: String(body.audit_id ?? ""),
|
|
37
|
+
tenant,
|
|
38
|
+
intentId,
|
|
39
|
+
code,
|
|
40
|
+
message: body.message != null ? String(body.message) : undefined,
|
|
41
|
+
decisionId: body.decision_id != null ? String(body.decision_id) : undefined,
|
|
42
|
+
approvalRequestId: body.approval_request_id != null ? String(body.approval_request_id) : undefined,
|
|
43
|
+
policyVersion: typeof body.policy_version === "number" ? body.policy_version : undefined,
|
|
44
|
+
reasonCodes,
|
|
45
|
+
spendScope,
|
|
46
|
+
remainingCents: typeof body.remaining_cents === "number" ? body.remaining_cents : undefined,
|
|
47
|
+
retryAfter: typeof body.retry_after === "number" ? body.retry_after : undefined,
|
|
48
|
+
approvalRequired,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function verifyCapabilityPayload(input) {
|
|
52
|
+
const payload = {
|
|
53
|
+
intent_id: input.intentId,
|
|
54
|
+
token: input.token,
|
|
55
|
+
operation: input.operation,
|
|
56
|
+
requested_spend_cents: input.requestedSpendCents ?? 0,
|
|
57
|
+
};
|
|
58
|
+
if (input.vendorId?.trim())
|
|
59
|
+
payload.vendor_id = input.vendorId.trim();
|
|
60
|
+
if (input.taskId?.trim())
|
|
61
|
+
payload.task_id = input.taskId.trim();
|
|
62
|
+
if (input.workflowId?.trim())
|
|
63
|
+
payload.workflow_id = input.workflowId.trim();
|
|
64
|
+
if (input.toolCallId?.trim())
|
|
65
|
+
payload.tool_call_id = input.toolCallId.trim();
|
|
66
|
+
if (input.toolName?.trim())
|
|
67
|
+
payload.tool_name = input.toolName.trim();
|
|
68
|
+
if (input.currency?.trim())
|
|
69
|
+
payload.currency = input.currency.trim();
|
|
70
|
+
if (input.agentSubject?.trim())
|
|
71
|
+
payload.agent_subject = input.agentSubject.trim();
|
|
72
|
+
if (input.approvalToken?.trim())
|
|
73
|
+
payload.approval_token = input.approvalToken.trim();
|
|
74
|
+
if (input.idempotencyKey?.trim())
|
|
75
|
+
payload.idempotency_key = input.idempotencyKey.trim();
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
7
78
|
export const DEFAULT_PAYBOND_GATEWAY_BASE_URL = "https://api.paybond.ai";
|
|
8
79
|
function defaultGatewayBaseUrl(value) {
|
|
9
80
|
const trimmed = value?.trim();
|
|
@@ -158,16 +229,24 @@ export class PaybondCapabilityBinding {
|
|
|
158
229
|
}
|
|
159
230
|
async verifySpendCapability(input) {
|
|
160
231
|
return this.harbor.verifyCapability({
|
|
232
|
+
...input,
|
|
161
233
|
intentId: this.intentId,
|
|
162
234
|
token: this.capabilityToken,
|
|
163
|
-
operation: input.operation,
|
|
164
|
-
requestedSpendCents: input.requestedSpendCents,
|
|
165
235
|
});
|
|
166
236
|
}
|
|
167
237
|
async authorizeSpend(input) {
|
|
168
238
|
return this.verifySpendCapability(input);
|
|
169
239
|
}
|
|
170
240
|
}
|
|
241
|
+
export class PaybondSpendApprovalRequiredError extends Error {
|
|
242
|
+
result;
|
|
243
|
+
constructor(result) {
|
|
244
|
+
const reason = result.message ?? result.code ?? "approval_required";
|
|
245
|
+
super(`Paybond spend authorization requires approval: ${reason}`);
|
|
246
|
+
this.name = "PaybondSpendApprovalRequiredError";
|
|
247
|
+
this.result = result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
171
250
|
export class PaybondSpendDeniedError extends Error {
|
|
172
251
|
result;
|
|
173
252
|
constructor(result) {
|
|
@@ -188,10 +267,9 @@ export class PaybondSpendGuard {
|
|
|
188
267
|
}
|
|
189
268
|
async verifySpendCapability(input) {
|
|
190
269
|
return this.harbor.verifyCapability({
|
|
270
|
+
...input,
|
|
191
271
|
intentId: this.intentId,
|
|
192
272
|
token: this.capabilityToken,
|
|
193
|
-
operation: input.operation,
|
|
194
|
-
requestedSpendCents: input.requestedSpendCents,
|
|
195
273
|
});
|
|
196
274
|
}
|
|
197
275
|
async authorizeSpend(input) {
|
|
@@ -200,14 +278,42 @@ export class PaybondSpendGuard {
|
|
|
200
278
|
async assertSpendAuthorized(input) {
|
|
201
279
|
const result = await this.authorizeSpend(input);
|
|
202
280
|
if (!result.allow) {
|
|
281
|
+
if (result.approvalRequired) {
|
|
282
|
+
throw new PaybondSpendApprovalRequiredError(result);
|
|
283
|
+
}
|
|
203
284
|
throw new PaybondSpendDeniedError(result);
|
|
204
285
|
}
|
|
205
286
|
return result;
|
|
206
287
|
}
|
|
288
|
+
/** Finalizes scope reservations tied to an authorization decision after tool execution. */
|
|
289
|
+
async completeSpendAuthorization(decisionId, outcome) {
|
|
290
|
+
const complete = this.harbor.completeSpendDecision;
|
|
291
|
+
if (!complete) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await complete.call(this.harbor, { decisionId, outcome });
|
|
295
|
+
}
|
|
207
296
|
guardTool(input, handler) {
|
|
208
297
|
return async (...args) => {
|
|
209
|
-
await this.assertSpendAuthorized(input);
|
|
210
|
-
|
|
298
|
+
const auth = await this.assertSpendAuthorized(input);
|
|
299
|
+
try {
|
|
300
|
+
const result = await handler(...args);
|
|
301
|
+
if (auth.decisionId) {
|
|
302
|
+
await this.completeSpendAuthorization(auth.decisionId, "consumed");
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (auth.decisionId) {
|
|
308
|
+
try {
|
|
309
|
+
await this.completeSpendAuthorization(auth.decisionId, "released");
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Best-effort release when the guarded handler fails.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
211
317
|
};
|
|
212
318
|
}
|
|
213
319
|
}
|
|
@@ -245,6 +351,9 @@ export function paybondRuntimeToolCallAdapter(init) {
|
|
|
245
351
|
if (init.onDeny) {
|
|
246
352
|
return await init.onDeny(result, call);
|
|
247
353
|
}
|
|
354
|
+
if (result.approvalRequired) {
|
|
355
|
+
throw new PaybondSpendApprovalRequiredError(result);
|
|
356
|
+
}
|
|
248
357
|
throw new PaybondSpendDeniedError(result);
|
|
249
358
|
}
|
|
250
359
|
return await init.execute(call);
|
|
@@ -384,17 +493,13 @@ export class HarborClient {
|
|
|
384
493
|
*/
|
|
385
494
|
async verifyCapability(input) {
|
|
386
495
|
const url = `${this.base}verify`;
|
|
387
|
-
const payload =
|
|
388
|
-
intent_id: input.intentId,
|
|
389
|
-
token: input.token,
|
|
390
|
-
operation: input.operation,
|
|
391
|
-
requested_spend_cents: input.requestedSpendCents ?? 0,
|
|
392
|
-
};
|
|
496
|
+
const payload = verifyCapabilityPayload(input);
|
|
393
497
|
const res = await this.fetchWithRetries(url, {
|
|
394
498
|
method: "POST",
|
|
395
499
|
headers: {
|
|
396
500
|
"content-type": "application/json",
|
|
397
501
|
"x-tenant-id": this.tenantId,
|
|
502
|
+
...(input.idempotencyKey?.trim() ? { "idempotency-key": input.idempotencyKey.trim() } : {}),
|
|
398
503
|
},
|
|
399
504
|
body: JSON.stringify(payload),
|
|
400
505
|
}, { retryBody: payload });
|
|
@@ -407,20 +512,7 @@ export class HarborClient {
|
|
|
407
512
|
});
|
|
408
513
|
}
|
|
409
514
|
const body = JSON.parse(text);
|
|
410
|
-
|
|
411
|
-
throw new Error(`verify tenant mismatch: client=${this.tenantId} harbor=${body.tenant}`);
|
|
412
|
-
}
|
|
413
|
-
if (body.intent_id !== input.intentId) {
|
|
414
|
-
throw new Error(`verify intent mismatch: requested=${input.intentId} harbor=${body.intent_id}`);
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
allow: body.allow,
|
|
418
|
-
auditId: body.audit_id,
|
|
419
|
-
tenant: body.tenant,
|
|
420
|
-
intentId: body.intent_id,
|
|
421
|
-
code: body.code,
|
|
422
|
-
message: body.message,
|
|
423
|
-
};
|
|
515
|
+
return parseVerifyCapabilityBody(body, this.tenantId, input.intentId);
|
|
424
516
|
}
|
|
425
517
|
async verifySpendCapability(input) {
|
|
426
518
|
return this.verifyCapability(input);
|
|
@@ -722,13 +814,12 @@ export class GatewayHarborClient {
|
|
|
722
814
|
});
|
|
723
815
|
}
|
|
724
816
|
async verifyCapability(input) {
|
|
725
|
-
const payload =
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
};
|
|
731
|
-
const { res, text, url } = await this.postJSON("/verify", payload);
|
|
817
|
+
const payload = verifyCapabilityPayload(input);
|
|
818
|
+
const headers = {};
|
|
819
|
+
if (input.idempotencyKey?.trim()) {
|
|
820
|
+
headers["idempotency-key"] = input.idempotencyKey.trim();
|
|
821
|
+
}
|
|
822
|
+
const { res, text, url } = await this.postJSON("/verify", payload, headers);
|
|
732
823
|
if (!res.ok) {
|
|
733
824
|
throw new HarborHttpError(`Gateway verify HTTP ${res.status}: ${text}`, {
|
|
734
825
|
statusCode: res.status,
|
|
@@ -737,20 +828,7 @@ export class GatewayHarborClient {
|
|
|
737
828
|
});
|
|
738
829
|
}
|
|
739
830
|
const body = JSON.parse(text);
|
|
740
|
-
|
|
741
|
-
throw new Error(`verify tenant mismatch: client=${this.tenantId} gateway=${body.tenant}`);
|
|
742
|
-
}
|
|
743
|
-
if (body.intent_id !== input.intentId) {
|
|
744
|
-
throw new Error(`verify intent mismatch: requested=${input.intentId} gateway=${body.intent_id}`);
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
allow: body.allow,
|
|
748
|
-
auditId: body.audit_id,
|
|
749
|
-
tenant: body.tenant,
|
|
750
|
-
intentId: body.intent_id,
|
|
751
|
-
code: body.code,
|
|
752
|
-
message: body.message,
|
|
753
|
-
};
|
|
831
|
+
return parseVerifyCapabilityBody(body, this.tenantId, input.intentId);
|
|
754
832
|
}
|
|
755
833
|
async verifySpendCapability(input) {
|
|
756
834
|
return this.verifyCapability(input);
|
|
@@ -758,6 +836,17 @@ export class GatewayHarborClient {
|
|
|
758
836
|
async authorizeSpend(input) {
|
|
759
837
|
return this.verifyCapability(input);
|
|
760
838
|
}
|
|
839
|
+
/** Finalizes active spend reservations after tool execution completes or is aborted. */
|
|
840
|
+
async completeSpendDecision(input) {
|
|
841
|
+
const { res, text, url } = await this.postJSON(`/v1/spend/decisions/${encodeURIComponent(input.decisionId)}/complete`, { outcome: input.outcome });
|
|
842
|
+
if (!res.ok) {
|
|
843
|
+
throw new HarborHttpError(`Gateway spend complete HTTP ${res.status}: ${text}`, {
|
|
844
|
+
statusCode: res.status,
|
|
845
|
+
url,
|
|
846
|
+
bodyText: text,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
761
850
|
async createIntent(body, options) {
|
|
762
851
|
const { res, text, url } = await this.postJSON("/harbor/intents", body, this.mutationHeaders("createIntent", options));
|
|
763
852
|
if (!res.ok) {
|
package/dist/init.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { runCli } from "./cli/router.js";
|
|
2
6
|
const FRAMEWORKS = new Set([
|
|
3
7
|
"generic",
|
|
4
8
|
"provider-agnostic",
|
|
@@ -79,7 +83,8 @@ function parseArgs(argv) {
|
|
|
79
83
|
return { preset, framework, out, force };
|
|
80
84
|
}
|
|
81
85
|
function template(framework) {
|
|
82
|
-
return `import
|
|
86
|
+
return `import fs from "node:fs/promises";
|
|
87
|
+
import {
|
|
83
88
|
Paybond,
|
|
84
89
|
type SandboxGuardrailBootstrapResult,
|
|
85
90
|
type SandboxGuardrailEvidenceResult,
|
|
@@ -139,8 +144,6 @@ function readEnvValue(body: string, key: string): string | undefined {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
async function readTextFile(envFile: string): Promise<string | undefined> {
|
|
142
|
-
// @ts-ignore Node builtins are available in agent and CLI Node runtimes.
|
|
143
|
-
const fs: { readFile(path: string, encoding: "utf8"): Promise<string> } = await import("node:fs/promises");
|
|
144
147
|
try {
|
|
145
148
|
return await fs.readFile(envFile, "utf8");
|
|
146
149
|
} catch (err) {
|
|
@@ -239,8 +242,6 @@ export async function submitSandboxEvidence(
|
|
|
239
242
|
`;
|
|
240
243
|
}
|
|
241
244
|
async function writeScaffold(out, body, force) {
|
|
242
|
-
// @ts-expect-error Node builtins are available in the published CLI runtime.
|
|
243
|
-
const fs = await import("node:fs/promises");
|
|
244
245
|
try {
|
|
245
246
|
await fs.stat(out);
|
|
246
247
|
if (!force) {
|
|
@@ -286,12 +287,6 @@ async function invokedFromCLI() {
|
|
|
286
287
|
if (!scriptPath) {
|
|
287
288
|
return false;
|
|
288
289
|
}
|
|
289
|
-
// @ts-ignore Node builtins are available in the published CLI runtime.
|
|
290
|
-
const fs = (await import("node:fs/promises"));
|
|
291
|
-
// @ts-ignore Node builtins are available in the published CLI runtime.
|
|
292
|
-
const path = (await import("node:path"));
|
|
293
|
-
// @ts-ignore Node builtins are available in the published CLI runtime.
|
|
294
|
-
const url = (await import("node:url"));
|
|
295
290
|
async function realFileURL(filePath) {
|
|
296
291
|
let resolved = path.resolve(filePath);
|
|
297
292
|
try {
|
|
@@ -301,15 +296,15 @@ async function invokedFromCLI() {
|
|
|
301
296
|
// If realpath fails, compare the absolute path. This keeps direct execution
|
|
302
297
|
// working even when the script path disappears during process startup.
|
|
303
298
|
}
|
|
304
|
-
return normalizeFileURL(
|
|
299
|
+
return normalizeFileURL(pathToFileURL(resolved).href);
|
|
305
300
|
}
|
|
306
|
-
return (await realFileURL(scriptPath)) === (await realFileURL(
|
|
301
|
+
return (await realFileURL(scriptPath)) === (await realFileURL(fileURLToPath(import.meta.url)));
|
|
307
302
|
}
|
|
308
303
|
invokedFromCLI().then((invoked) => {
|
|
309
304
|
if (!invoked) {
|
|
310
305
|
return;
|
|
311
306
|
}
|
|
312
|
-
|
|
307
|
+
runCli(["init", "guardrail", ...process.argv.slice(2)]).then((code) => {
|
|
313
308
|
process.exitCode = code;
|
|
314
309
|
}, (err) => {
|
|
315
310
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|