knoxcall 0.0.1 → 1.0.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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +439 -2
- data/exe/knoxcall +8 -0
- data/lib/knoxcall/bootstrap.rb +70 -0
- data/lib/knoxcall/bound_route.rb +47 -0
- data/lib/knoxcall/cli/ai.rb +79 -0
- data/lib/knoxcall/cli/ai_control.rb +275 -0
- data/lib/knoxcall/cli/common.rb +96 -0
- data/lib/knoxcall/cli/init.rb +94 -0
- data/lib/knoxcall/cli/login.rb +306 -0
- data/lib/knoxcall/cli/logout.rb +41 -0
- data/lib/knoxcall/cli/whoami.rb +29 -0
- data/lib/knoxcall/cli.rb +377 -0
- data/lib/knoxcall/client.rb +1025 -0
- data/lib/knoxcall/credentials_file.rb +442 -0
- data/lib/knoxcall/dpop.rb +79 -0
- data/lib/knoxcall/egress_observations.rb +372 -0
- data/lib/knoxcall/errors.rb +304 -0
- data/lib/knoxcall/intercept_patch.rb +181 -0
- data/lib/knoxcall/intercept_pipeline.rb +455 -0
- data/lib/knoxcall/intercept_resolver.rb +140 -0
- data/lib/knoxcall/intercept_store.rb +203 -0
- data/lib/knoxcall/login.rb +144 -0
- data/lib/knoxcall/resources/account.rb +12 -0
- data/lib/knoxcall/resources/agents.rb +25 -0
- data/lib/knoxcall/resources/ai_gateway.rb +417 -0
- data/lib/knoxcall/resources/api_keys.rb +35 -0
- data/lib/knoxcall/resources/audit_logs.rb +45 -0
- data/lib/knoxcall/resources/clients.rb +39 -0
- data/lib/knoxcall/resources/crypto.rb +122 -0
- data/lib/knoxcall/resources/dynamic_db.rb +68 -0
- data/lib/knoxcall/resources/environments.rb +16 -0
- data/lib/knoxcall/resources/logs.rb +51 -0
- data/lib/knoxcall/resources/oauth_clients.rb +34 -0
- data/lib/knoxcall/resources/opportunities.rb +61 -0
- data/lib/knoxcall/resources/pki.rb +41 -0
- data/lib/knoxcall/resources/roles.rb +27 -0
- data/lib/knoxcall/resources/routes.rb +53 -0
- data/lib/knoxcall/resources/secrets.rb +98 -0
- data/lib/knoxcall/resources/unwraps_envelope.rb +90 -0
- data/lib/knoxcall/resources/vaults.rb +77 -0
- data/lib/knoxcall/resources/webhooks.rb +48 -0
- data/lib/knoxcall/resources/workflows.rb +86 -0
- data/lib/knoxcall/resources/wrap.rb +352 -0
- data/lib/knoxcall/route_refusal.rb +67 -0
- data/lib/knoxcall/signup.rb +122 -0
- data/lib/knoxcall/token_exchange.rb +169 -0
- data/lib/knoxcall/ulid.rb +19 -0
- data/lib/knoxcall/warnings.rb +63 -0
- data/lib/knoxcall/workload_provider.rb +192 -0
- data/lib/knoxcall/wrap_faraday_adapter.rb +119 -0
- data/lib/knoxcall/wrap_faraday_middleware.rb +67 -0
- data/lib/knoxcall/wrap_transport.rb +139 -0
- data/lib/knoxcall.rb +45 -1
- metadata +70 -9
data/lib/knoxcall/cli.rb
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
require "knoxcall"
|
|
4
|
+
require "knoxcall/cli/common"
|
|
5
|
+
require "knoxcall/cli/login"
|
|
6
|
+
require "knoxcall/cli/logout"
|
|
7
|
+
require "knoxcall/cli/whoami"
|
|
8
|
+
require "knoxcall/cli/init"
|
|
9
|
+
require "knoxcall/cli/ai"
|
|
10
|
+
require "knoxcall/cli/ai_control"
|
|
11
|
+
|
|
12
|
+
module KnoxCall
|
|
13
|
+
# KnoxCall CLI — `knoxcall login` / `logout` / `whoami` / `init` / `ai`.
|
|
14
|
+
#
|
|
15
|
+
# Shipped as the gem executable (`exe/knoxcall`); `KnoxCall::CLI.run(argv)`
|
|
16
|
+
# is the testable entry point. Credentials are stored in the cross-SDK
|
|
17
|
+
# ~/.knoxcall/credentials.json file and picked up automatically by every
|
|
18
|
+
# KnoxCall SDK (auto-detect slot 2). The command surface, messages, and exit
|
|
19
|
+
# codes mirror the python reference implementation (PARITY §13).
|
|
20
|
+
module CLI
|
|
21
|
+
PROGRAM = "knoxcall"
|
|
22
|
+
COMMANDS = %w[login logout whoami init ai].freeze
|
|
23
|
+
# `ai` is the only command with a sub-command of its own.
|
|
24
|
+
#
|
|
25
|
+
# AIGW-162. `exchange` is the data-plane door and needs no login; the rest
|
|
26
|
+
# are the control plane and act as the signed-in tenant. Both live under
|
|
27
|
+
# `ai` because they are one surface to a user, and the golden path crosses
|
|
28
|
+
# between them: create-agent -> mint -> a real call.
|
|
29
|
+
#
|
|
30
|
+
# Every one of these takes FLAGS ONLY, no positionals — four of the five SDK
|
|
31
|
+
# CLIs hand-roll their parser and reject positionals outright (only python
|
|
32
|
+
# gets them free from argparse), so an id as a positional would be a surface
|
|
33
|
+
# that is the same in all five except in shape.
|
|
34
|
+
AI_COMMANDS = %w[exchange gateways agents create-agent mint usage].freeze
|
|
35
|
+
AI_COMMAND_SUMMARIES = {
|
|
36
|
+
"exchange" => "exchange a CI OIDC token for a capability token (no login needed)",
|
|
37
|
+
"gateways" => "list AI gateways",
|
|
38
|
+
"agents" => "list a gateway's agents",
|
|
39
|
+
"create-agent" => "create an agent with its upstream credential",
|
|
40
|
+
"mint" => "mint a capability token (shown once)",
|
|
41
|
+
"usage" => "cost + token usage by model"
|
|
42
|
+
}.freeze
|
|
43
|
+
USAGE = "usage: #{PROGRAM} [-h] {#{COMMANDS.join(',')}} ..."
|
|
44
|
+
DESCRIPTION =
|
|
45
|
+
"KnoxCall command-line interface — sign in once, every SDK on this machine picks it up."
|
|
46
|
+
COMMAND_SUMMARIES = {
|
|
47
|
+
"login" => "sign in with your browser and store credentials locally",
|
|
48
|
+
"logout" => "revoke and remove stored credentials",
|
|
49
|
+
"whoami" => "show the signed-in tenant",
|
|
50
|
+
"init" => "get started wrapping a provider SDK (escrow a key)",
|
|
51
|
+
"ai" => "AI gateway operations"
|
|
52
|
+
}.freeze
|
|
53
|
+
PROFILE_HELP = "credentials profile name (default: KNOXCALL_PROFILE or 'default')"
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# Run the CLI: 0 on success, 1 on expected failure/interrupt ("error: …" /
|
|
58
|
+
# "aborted" on stderr, never a backtrace), 2 on usage errors.
|
|
59
|
+
def run(argv = ARGV)
|
|
60
|
+
parsed = parse(Array(argv).map(&:to_s))
|
|
61
|
+
return parsed if parsed.is_a?(Integer) # help printed (0) or usage error (2)
|
|
62
|
+
|
|
63
|
+
command, options = parsed
|
|
64
|
+
begin
|
|
65
|
+
execute(command, options)
|
|
66
|
+
rescue Error, KnoxCall::Error => e
|
|
67
|
+
warn "error: #{e.message}"
|
|
68
|
+
1
|
|
69
|
+
rescue Interrupt
|
|
70
|
+
warn "aborted"
|
|
71
|
+
1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def execute(command, options)
|
|
76
|
+
case command
|
|
77
|
+
when "login" then Login.run(options)
|
|
78
|
+
when "logout" then Logout.run(options)
|
|
79
|
+
when "whoami" then Whoami.run(options)
|
|
80
|
+
when "init" then Init.run(options)
|
|
81
|
+
when "ai" then execute_ai(options)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# `ai` fans out to its own sub-commands. `exchange` is the data-plane door
|
|
86
|
+
# (no login); the other five are the control plane and act as the
|
|
87
|
+
# signed-in tenant.
|
|
88
|
+
def execute_ai(options)
|
|
89
|
+
case options[:ai_command]
|
|
90
|
+
when "exchange" then Ai.run(options)
|
|
91
|
+
when "gateways" then AiControl.gateways(options)
|
|
92
|
+
when "agents" then AiControl.agents(options)
|
|
93
|
+
when "create-agent" then AiControl.create_agent(options)
|
|
94
|
+
when "mint" then AiControl.mint(options)
|
|
95
|
+
when "usage" then AiControl.usage(options)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Parse argv into [command, options]. Help and usage errors are handled
|
|
100
|
+
# here: help prints to stdout and returns 0; a usage error prints to
|
|
101
|
+
# stderr and returns 2 (matching the python reference's argparse).
|
|
102
|
+
def parse(argv)
|
|
103
|
+
if argv.empty?
|
|
104
|
+
warn USAGE
|
|
105
|
+
warn "#{PROGRAM}: error: a command is required (choose from #{COMMANDS.join(', ')})"
|
|
106
|
+
return 2
|
|
107
|
+
end
|
|
108
|
+
if %w[-h --help].include?(argv.first)
|
|
109
|
+
puts root_help
|
|
110
|
+
return 0
|
|
111
|
+
end
|
|
112
|
+
command = argv.first
|
|
113
|
+
unless COMMANDS.include?(command)
|
|
114
|
+
warn USAGE
|
|
115
|
+
warn "#{PROGRAM}: error: invalid choice: '#{command}' (choose from #{COMMANDS.join(', ')})"
|
|
116
|
+
return 2
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
options = {}
|
|
120
|
+
rest_argv = argv[1..]
|
|
121
|
+
ai_command = nil
|
|
122
|
+
|
|
123
|
+
# `ai` carries a sub-command. Consume it here, then fall through to the
|
|
124
|
+
# same OptionParser path with argv advanced past it — one parser, not two.
|
|
125
|
+
if command == "ai"
|
|
126
|
+
sub = rest_argv.first
|
|
127
|
+
if sub.nil?
|
|
128
|
+
warn ai_usage
|
|
129
|
+
warn "#{PROGRAM} ai: error: a sub-command is required (choose from #{AI_COMMANDS.join(', ')})"
|
|
130
|
+
return 2
|
|
131
|
+
end
|
|
132
|
+
if %w[-h --help].include?(sub)
|
|
133
|
+
puts ai_help
|
|
134
|
+
return 0
|
|
135
|
+
end
|
|
136
|
+
unless AI_COMMANDS.include?(sub)
|
|
137
|
+
warn ai_usage
|
|
138
|
+
warn "#{PROGRAM} ai: error: invalid choice: '#{sub}' (choose from #{AI_COMMANDS.join(', ')})"
|
|
139
|
+
return 2
|
|
140
|
+
end
|
|
141
|
+
ai_command = sub
|
|
142
|
+
options[:ai_command] = sub
|
|
143
|
+
rest_argv = rest_argv[1..]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
label = ai_command ? "#{command} #{ai_command}" : command
|
|
147
|
+
help_requested = false
|
|
148
|
+
parser = build_parser(command, options, ai_command)
|
|
149
|
+
parser.on("-h", "--help", "show this help message and exit") { help_requested = true }
|
|
150
|
+
begin
|
|
151
|
+
rest = parser.parse(rest_argv)
|
|
152
|
+
rescue OptionParser::ParseError => e
|
|
153
|
+
warn parser.banner
|
|
154
|
+
warn "#{PROGRAM} #{label}: error: #{e.message}"
|
|
155
|
+
return 2
|
|
156
|
+
end
|
|
157
|
+
if help_requested
|
|
158
|
+
puts parser
|
|
159
|
+
return 0
|
|
160
|
+
end
|
|
161
|
+
unless rest.empty?
|
|
162
|
+
warn parser.banner
|
|
163
|
+
warn "#{PROGRAM} #{label}: error: unrecognized arguments: #{rest.join(' ')}"
|
|
164
|
+
return 2
|
|
165
|
+
end
|
|
166
|
+
[command, options]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def ai_usage
|
|
170
|
+
"usage: #{PROGRAM} ai [-h] {#{AI_COMMANDS.join(',')}} ..."
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def ai_help
|
|
174
|
+
<<~HELP
|
|
175
|
+
#{ai_usage}
|
|
176
|
+
|
|
177
|
+
AI-gateway operations.
|
|
178
|
+
|
|
179
|
+
From a tenant with nothing in it to a real streamed call, in two commands:
|
|
180
|
+
|
|
181
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
182
|
+
knoxcall ai create-agent --name copilot --slug copilot \\
|
|
183
|
+
--provider anthropic --secret-from-env ANTHROPIC_API_KEY
|
|
184
|
+
knoxcall ai mint --agent <id>
|
|
185
|
+
|
|
186
|
+
sub-commands:
|
|
187
|
+
#{AI_COMMANDS.map { |c| format(' %-14s %s', c, AI_COMMAND_SUMMARIES[c]) }.join("\n")}
|
|
188
|
+
|
|
189
|
+
options:
|
|
190
|
+
-h, --help show this help message and exit
|
|
191
|
+
HELP
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def root_help
|
|
195
|
+
<<~HELP
|
|
196
|
+
#{USAGE}
|
|
197
|
+
|
|
198
|
+
#{DESCRIPTION}
|
|
199
|
+
|
|
200
|
+
commands:
|
|
201
|
+
#{COMMANDS.map { |c| format(' %-8s %s', c, COMMAND_SUMMARIES[c]) }.join("\n")}
|
|
202
|
+
|
|
203
|
+
options:
|
|
204
|
+
-h, --help show this help message and exit
|
|
205
|
+
HELP
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def build_parser(command, options, ai_command = nil)
|
|
209
|
+
OptionParser.new do |o|
|
|
210
|
+
o.banner = "usage: #{PROGRAM} #{command} [options]"
|
|
211
|
+
o.summary_width = 18
|
|
212
|
+
case command
|
|
213
|
+
when "login"
|
|
214
|
+
o.on("--tenant SLUG", "tenant slug hint for the sign-in page") do |v|
|
|
215
|
+
options[:tenant] = v
|
|
216
|
+
end
|
|
217
|
+
o.on("--base-url URL",
|
|
218
|
+
"management API base URL (default https://api.knoxcall.com, or KNOXCALL_BASE_URL)") do |v|
|
|
219
|
+
options[:base_url] = v
|
|
220
|
+
end
|
|
221
|
+
o.on("--sandbox", "log in against the sandbox environment") do
|
|
222
|
+
options[:sandbox] = true
|
|
223
|
+
end
|
|
224
|
+
o.on("--profile NAME", PROFILE_HELP) { |v| options[:profile] = v }
|
|
225
|
+
o.on("--device", "use the device-code flow (headless/SSH machines)") do
|
|
226
|
+
options[:device] = true
|
|
227
|
+
end
|
|
228
|
+
o.on("--no-browser", "never open a browser (implies the device-code flow)") do
|
|
229
|
+
options[:no_browser] = true
|
|
230
|
+
end
|
|
231
|
+
when "logout", "whoami"
|
|
232
|
+
o.on("--profile NAME", PROFILE_HELP) { |v| options[:profile] = v }
|
|
233
|
+
when "init"
|
|
234
|
+
o.on("--profile NAME", PROFILE_HELP) { |v| options[:profile] = v }
|
|
235
|
+
o.on("--base-url URL",
|
|
236
|
+
"management API base URL (default https://api.knoxcall.com)") do |v|
|
|
237
|
+
options[:base_url] = v
|
|
238
|
+
end
|
|
239
|
+
o.on("--sandbox", "operate against the sandbox environment") do
|
|
240
|
+
options[:sandbox] = true
|
|
241
|
+
end
|
|
242
|
+
o.on("--provider PROVIDER", "provider to escrow a key for (e.g. stripe); enables escrow mode") do |v|
|
|
243
|
+
options[:provider] = v
|
|
244
|
+
end
|
|
245
|
+
o.on("--secret-name NAME", "name for the escrowed credential (required with --provider)") do |v|
|
|
246
|
+
options[:secret_name] = v
|
|
247
|
+
end
|
|
248
|
+
o.on("--host HOST", "upstream host to pin the credential to (required with --provider)") do |v|
|
|
249
|
+
options[:host] = v
|
|
250
|
+
end
|
|
251
|
+
when "ai"
|
|
252
|
+
o.banner = "usage: #{PROGRAM} ai #{ai_command} [options]"
|
|
253
|
+
build_ai_parser(o, options, ai_command)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The `ai` flag table is keyed by SUB-command, not by `ai`.
|
|
259
|
+
#
|
|
260
|
+
# A single shared table would accept `ai exchange --period 30d` and silently
|
|
261
|
+
# ignore it — the opposite of what every other command here does with an
|
|
262
|
+
# unknown flag (usage error, exit 2). It also has to be per-sub-command for
|
|
263
|
+
# `--secret-value` to be REJECTED on create-agent: an unknown flag is only
|
|
264
|
+
# unknown if the table it is checked against is the one for that command.
|
|
265
|
+
def build_ai_parser(opt, options, ai_command)
|
|
266
|
+
case ai_command
|
|
267
|
+
when "exchange"
|
|
268
|
+
opt.on("--tenant SLUG", "tenant slug; the data-plane host is https://{tenant}.knoxcall.com") do |v|
|
|
269
|
+
options[:tenant] = v
|
|
270
|
+
end
|
|
271
|
+
opt.on("--sandbox", "use the Test data space (sandbox-{tenant}.knoxcall.com)") do
|
|
272
|
+
options[:sandbox] = true
|
|
273
|
+
end
|
|
274
|
+
opt.on("--base-url URL", "full data-plane origin; overrides --tenant") do |v|
|
|
275
|
+
options[:base_url] = v
|
|
276
|
+
end
|
|
277
|
+
# Assigned even when empty: the key's PRESENCE is what says the
|
|
278
|
+
# caller asked for a resource, and an empty one is a server refusal
|
|
279
|
+
# rather than "no resource".
|
|
280
|
+
opt.on("--resource URI",
|
|
281
|
+
"RFC 8707 resource indicator (an MCP server's `resource`); narrows the token to that one MCP server") do |v|
|
|
282
|
+
options[:resource] = v
|
|
283
|
+
end
|
|
284
|
+
opt.on("--audience AUDIENCE", "defaults to knoxcall:gateway") do |v|
|
|
285
|
+
options[:audience] = v
|
|
286
|
+
end
|
|
287
|
+
when "gateways"
|
|
288
|
+
ai_common_options(opt, options)
|
|
289
|
+
when "agents"
|
|
290
|
+
opt.on("--gateway ID", "gateway id") { |v| options[:gateway] = v }
|
|
291
|
+
ai_common_options(opt, options)
|
|
292
|
+
when "create-agent"
|
|
293
|
+
# Printed by `--help` only — OptionParser#banner, which is what a usage
|
|
294
|
+
# error echoes, stays the single usage line.
|
|
295
|
+
ai_create_agent_preamble(opt)
|
|
296
|
+
opt.on("--slug SLUG", "url slug; the agent is served at /v1/ai/{slug}") do |v|
|
|
297
|
+
options[:slug] = v
|
|
298
|
+
end
|
|
299
|
+
opt.on("--provider PROVIDER",
|
|
300
|
+
"provider id (anthropic, openai, bedrock, …); catalog is server-side") do |v|
|
|
301
|
+
options[:provider] = v
|
|
302
|
+
end
|
|
303
|
+
opt.on("--secret ID", "id of an existing KnoxCall secret holding the key") do |v|
|
|
304
|
+
options[:secret] = v
|
|
305
|
+
end
|
|
306
|
+
# The key is read from the NAMED ENVIRONMENT VARIABLE, never from a
|
|
307
|
+
# flag value: an argv value lands in shell history, ps output and the
|
|
308
|
+
# CI log line that echoes the command.
|
|
309
|
+
opt.on("--secret-from-env VAR",
|
|
310
|
+
"env var holding the key; escrows it, reusing a same-named secret") do |v|
|
|
311
|
+
options[:secret_from_env] = v
|
|
312
|
+
end
|
|
313
|
+
opt.on("--name NAME", "display name (defaults to --slug)") { |v| options[:name] = v }
|
|
314
|
+
opt.on("--gateway ID", "gateway id or slug to create under") { |v| options[:gateway] = v }
|
|
315
|
+
opt.on("--model MODEL", "default model (required for openai-compatible)") do |v|
|
|
316
|
+
options[:model] = v
|
|
317
|
+
end
|
|
318
|
+
opt.on("--upstream URL",
|
|
319
|
+
"upstream base URL; required for azure-openai, ollama, " \
|
|
320
|
+
"bedrock and openai-compatible") do |v|
|
|
321
|
+
options[:upstream] = v
|
|
322
|
+
end
|
|
323
|
+
ai_common_options(opt, options)
|
|
324
|
+
when "mint"
|
|
325
|
+
opt.separator ""
|
|
326
|
+
opt.separator "Mint a capability token for an agent. The plaintext is returned ONCE and is"
|
|
327
|
+
opt.separator "the only thing on stdout, so it can be captured:"
|
|
328
|
+
opt.separator " TOKEN=\"$(knoxcall ai mint --agent ag_123)\""
|
|
329
|
+
opt.separator ""
|
|
330
|
+
opt.separator "options:"
|
|
331
|
+
opt.on("--agent ID", "agent id") { |v| options[:agent] = v }
|
|
332
|
+
opt.on("--kind KIND", "agent | read | tool | oneshot (default agent)") { |v| options[:kind] = v }
|
|
333
|
+
opt.on("--name NAME", "label for the token") { |v| options[:name] = v }
|
|
334
|
+
ai_common_options(opt, options)
|
|
335
|
+
when "usage"
|
|
336
|
+
opt.on("--period PERIOD", "7d | 30d | 90d (default 30d)") { |v| options[:period] = v }
|
|
337
|
+
opt.on("--agent ID", "scope to one agent") { |v| options[:agent] = v }
|
|
338
|
+
ai_common_options(opt, options)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The three rules `create-agent` exists to enforce, printed by `--help`.
|
|
343
|
+
# Separators land in the summary only, so a usage error still echoes the
|
|
344
|
+
# one-line banner rather than nine lines of prose.
|
|
345
|
+
def ai_create_agent_preamble(opt)
|
|
346
|
+
opt.separator ""
|
|
347
|
+
opt.separator "Create an agent wired to a provider credential, and print the command that"
|
|
348
|
+
opt.separator "follows. Works on a tenant with nothing in it: with no --gateway it uses your"
|
|
349
|
+
opt.separator "only gateway, or creates one when you have none. With several it refuses and"
|
|
350
|
+
opt.separator "lists them rather than picking one for you."
|
|
351
|
+
opt.separator ""
|
|
352
|
+
opt.separator "The provider key is read from the environment named by --secret-from-env,"
|
|
353
|
+
opt.separator "never from a flag — an argv value lands in shell history, ps output and the"
|
|
354
|
+
opt.separator "CI log. There is deliberately no --secret-value."
|
|
355
|
+
opt.separator ""
|
|
356
|
+
opt.separator "--provider and a credential are both required: the API accepts an agent with"
|
|
357
|
+
opt.separator "neither and stores one whose first data-plane call 502s."
|
|
358
|
+
opt.separator ""
|
|
359
|
+
opt.separator "Only the agent id goes to stdout, so it can be captured:"
|
|
360
|
+
opt.separator " AGENT=\"$(knoxcall ai create-agent --slug copilot --provider anthropic \\"
|
|
361
|
+
opt.separator " --secret-from-env ANTHROPIC_API_KEY)\""
|
|
362
|
+
opt.separator ""
|
|
363
|
+
opt.separator "options:"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Accepted by every control-plane sub-command: they select WHICH tenant and
|
|
367
|
+
# WHICH stored login is acting. `exchange` takes none of them — it needs no
|
|
368
|
+
# login at all.
|
|
369
|
+
def ai_common_options(opt, options)
|
|
370
|
+
opt.on("--profile NAME", PROFILE_HELP) { |v| options[:profile] = v }
|
|
371
|
+
opt.on("--base-url URL", "management API base URL (default https://api.knoxcall.com)") do |v|
|
|
372
|
+
options[:base_url] = v
|
|
373
|
+
end
|
|
374
|
+
opt.on("--sandbox", "operate against the Test data space") { options[:sandbox] = true }
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|