@pdpp/cli 0.1.0-beta.7 → 0.1.0-beta.8

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/README.md CHANGED
@@ -5,7 +5,7 @@ Command-line tools for PDPP providers.
5
5
  ## Status
6
6
 
7
7
  This package is the public npm home for the `pdpp` command. The beta CLI
8
- supports three command namespaces:
8
+ supports four command namespaces:
9
9
 
10
10
  - **`pdpp connect <provider-url>`** — delegated access: discovers provider
11
11
  metadata, self-registers a public client when the AS advertises dynamic
@@ -13,6 +13,47 @@ supports three command namespaces:
13
13
  stores scoped client credentials in the project-local `.pdpp/` cache without
14
14
  asking for an owner bearer token.
15
15
 
16
+ - **`pdpp owner-agent <onboard|status|control|connectors|setup|revoke>`** — trusted owner-agent
17
+ onboarding for a local agent that acts as the operator (for example Daisy).
18
+ This is owner-level local automation, deliberately separate from the default
19
+ grant-scoped `pdpp connect` path; ordinary agents should not use it.
20
+ `onboard <entrypoint-url>` discovers the `pdpp_owner_agent_onboarding`
21
+ advisory block (falling back to the RFC 8628 device-authorization shape in
22
+ authorization-server metadata), runs browser-mediated owner approval, and
23
+ writes the issued credential to a local file with `0600` permissions. The
24
+ bearer is never printed; only the verification URL, code, and non-secret
25
+ status are shown. Pass `--credential-file` to target Daisy's first supported
26
+ path `~/applications/daisy/.pi/agent/pdpp-owner-agent.json`; otherwise the
27
+ credential defaults to `~/.pdpp/owner-agents/<host>.json` and stores the
28
+ bearer as top-level `access_token` for local agents. `status` introspects the
29
+ stored credential. `control` lists the non-secret owner-agent control
30
+ capabilities (`GET /v1/owner/control`) and configured connection instances
31
+ (`GET /v1/owner/connections`) — each connection's `connection_id`, connector,
32
+ and label/label-needed state — so a trusted agent can discover what it can do
33
+ and what is configured without printing the bearer. `connectors list`,
34
+ `connectors search <query>`, and `connectors explain <connector-id>` read the
35
+ non-secret connector-template catalog (`GET /v1/owner/connector-templates`) so
36
+ a human or agent can discover Amazon/Gmail/Slack-like setup options before
37
+ starting anything. These discovery commands are read-only and do not mint
38
+ enrollment codes. `setup <connector-id>` is the start command: it requests the
39
+ same non-secret connection setup plan and next-step contract the console
40
+ add-source flow and owner-agent REST surface, by calling the shared server
41
+ planner (`POST /v1/owner/connections/intents`). It sends the stored bearer only
42
+ as an `Authorization` header, formats the plan's support state (`supported`,
43
+ `proof-gated`, `unsupported`, `deployment-blocked`), modality, and primary
44
+ owner next step, and surfaces owner-openable setup material (enrollment codes,
45
+ enroll endpoints, runbook paths) when present. Pass `--display-name <name>` to
46
+ label the resulting connection. No connection is created by this call; it
47
+ materializes only when the owner-mediated step completes. The setup plan never
48
+ includes provider secrets, owner cookies, browser cookies, or grant-scoped MCP
49
+ bearer material, and the bearer is never printed. `revoke` deletes its
50
+ dynamically registered client via the owner-session-gated RFC 7592 dashboard
51
+ path; run `pdpp ref login <authorization-server>` first or provide
52
+ `PDPP_OWNER_SESSION_COOKIE`. Owner-agent bearers are REST/control-plane
53
+ credentials; `/mcp` rejects them. Routine chat-hosted and task-scoped agents
54
+ should use the grant-scoped `pdpp connect` / MCP path instead, not an owner
55
+ bearer.
56
+
16
57
  - **`pdpp collector <advertise|enroll|run>`** — operator surface for the
17
58
  local collector runner. Pairs a host the operator controls (Claude Code or
18
59
  Codex CLI data) with a remote PDPP reference deployment via device-scoped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdpp/cli",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "Command-line tools for PDPP providers.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -10,6 +10,10 @@ import { runRefGrant } from './ref/commands/grant.js';
10
10
  import { runRefTrace } from './ref/commands/trace.js';
11
11
  import { runRefLogin } from './ref/commands/login.js';
12
12
  import { runRefConnectors } from './ref/commands/connectors.js';
13
+ import { runRefEventSubscriptions } from './ref/commands/event-subscriptions.js';
14
+ import { runRefCall } from './ref/commands/call.js';
15
+ import { readHelp, runRead } from './read/commands.js';
16
+ import { runOwnerAgent } from './owner-agent/command.js';
13
17
  import { PdppCliError, PdppUsageError } from './ref/errors.js';
14
18
 
15
19
  const HELP = `PDPP CLI
@@ -20,9 +24,17 @@ Usage:
20
24
  ${PDPP_CLI_BIN_NAME} connect <provider-url>
21
25
  ${PDPP_CLI_BIN_NAME} token <provider-url>
22
26
 
27
+ ${readHelp(PDPP_CLI_BIN_NAME)}
28
+
23
29
  Agent access:
24
30
  ${createPdppCliCommand()}
25
31
 
32
+ Trusted owner agent (owner-level local automation, not the default agent path):
33
+ ${PDPP_CLI_BIN_NAME} owner-agent onboard <entrypoint-url> [--credential-file <path>] [--client-id <id>] [--client-name <name>]
34
+ ${PDPP_CLI_BIN_NAME} owner-agent status [--credential-file <path>] [--entrypoint <url>]
35
+ ${PDPP_CLI_BIN_NAME} owner-agent control [--credential-file <path>] [--entrypoint <url>]
36
+ ${PDPP_CLI_BIN_NAME} owner-agent revoke [--credential-file <path>] [--entrypoint <url>] [--cache-root <dir>] [--owner-session <cookie>]
37
+
26
38
  Local collector (pair a host you control with a reference deployment):
27
39
  ${PDPP_CLI_BIN_NAME} collector advertise
28
40
  ${PDPP_CLI_BIN_NAME} collector enroll --base-url <url> --code <code>
@@ -30,11 +42,15 @@ Local collector (pair a host you control with a reference deployment):
30
42
 
31
43
  Reference diagnostics (reference server only):
32
44
  ${PDPP_CLI_BIN_NAME} ref login <reference-url>
45
+ ${PDPP_CLI_BIN_NAME} ref call <method> <path> --as-url <url> [--data <json> | --data-stdin]
33
46
  ${PDPP_CLI_BIN_NAME} ref run timeline <run-id> --as-url <url>
34
47
  ${PDPP_CLI_BIN_NAME} ref grant timeline <grant-id> --as-url <url>
35
48
  ${PDPP_CLI_BIN_NAME} ref trace show <trace-id> --as-url <url>
36
49
  ${PDPP_CLI_BIN_NAME} ref connectors list --as-url <url>
37
50
  ${PDPP_CLI_BIN_NAME} ref connectors show <connector-id> --as-url <url>
51
+ ${PDPP_CLI_BIN_NAME} ref event-subscriptions list --as-url <url> [--client-id <id>] [--grant-id <id>] [--status <status>]
52
+ ${PDPP_CLI_BIN_NAME} ref event-subscriptions show <subscription-id> --as-url <url>
53
+ ${PDPP_CLI_BIN_NAME} ref event-subscriptions disable <subscription-id> --as-url <url> [--reason <text>] [--yes]
38
54
 
39
55
  Notes:
40
56
  Do not ask users for owner bearer tokens for routine delegated access.
@@ -45,6 +61,11 @@ Notes:
45
61
  "pdpp ref login" caches an owner session in project-local .pdpp/ with mode 0600;
46
62
  later "pdpp ref" commands use the cache when --owner-session and
47
63
  PDPP_OWNER_SESSION_COOKIE are absent.
64
+ "pdpp ref call" is the escape hatch for owner POST/GET routes without a typed
65
+ command. It infers auth from the path: /_ref/* uses the owner session cookie,
66
+ /v1/owner/* uses the owner bearer (PDPP_OWNER_TOKEN or --owner-token-stdin).
67
+ Bodies are sent as JSON (CSRF-exempt server-side), so no _csrf parsing is
68
+ needed. Secrets are never printed.
48
69
  `;
49
70
 
50
71
  export async function runCli(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
@@ -106,31 +127,61 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces
106
127
  return await runCollector(rest, io);
107
128
  }
108
129
 
130
+ if (command === 'read') {
131
+ try {
132
+ return await runRead(rest, io);
133
+ } catch (error) {
134
+ if (error instanceof PdppUsageError) {
135
+ io.stderr.write(`${error.message}\n`);
136
+ return error.exitCode;
137
+ }
138
+ if (error instanceof PdppCliError) {
139
+ io.stderr.write(`${error.message}\n`);
140
+ return error.exitCode;
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ if (command === 'owner-agent') {
147
+ return await runOwnerAgent(rest, io);
148
+ }
149
+
109
150
  if (command === 'ref') {
110
151
  const [refCommand, ...refRest] = rest;
111
152
 
112
153
  if (!refCommand || refCommand === '--help' || refCommand === '-h') {
113
154
  io.stdout.write(`Reference diagnostics (reference server only):\n`);
114
155
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref login <reference-url> [--password-stdin] [--cache-root <dir>]\n`);
156
+ io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref call <method> <path> --as-url <url> [--data <json> | --data-stdin] [--auth cookie|bearer] [--owner-session <cookie>] [--owner-token-stdin] [--status-only] [--format json|table]\n`);
115
157
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref run timeline <run-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
116
158
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref grant timeline <grant-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
117
159
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref trace show <trace-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
118
160
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors list --as-url <url> [--owner-session <cookie>] [--format json|table] [--verbose]\n`);
119
161
  io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref connectors show <connector-id> --as-url <url> [--owner-session <cookie>] [--format json|table] [--verbose]\n`);
162
+ io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions list --as-url <url> [--client-id <id>] [--grant-id <id>] [--status <status>] [--owner-session <cookie>] [--format json|table]\n`);
163
+ io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions show <subscription-id> --as-url <url> [--owner-session <cookie>] [--format json|table]\n`);
164
+ io.stdout.write(` ${PDPP_CLI_BIN_NAME} ref event-subscriptions disable <subscription-id> --as-url <url> [--reason <text>] [--yes] [--owner-session <cookie>]\n`);
120
165
  io.stdout.write(`\nNotes:\n`);
121
166
  io.stdout.write(` "ref login" prompts the reference server's owner-login route and caches the\n`);
122
167
  io.stdout.write(` resulting session in .pdpp/owner-sessions/ (mode 0600). The cookie value is\n`);
123
168
  io.stdout.write(` never printed. The password must come from --password-stdin or\n`);
124
169
  io.stdout.write(` PDPP_OWNER_PASSWORD; it is not accepted on the command line.\n`);
170
+ io.stdout.write(` "ref call" infers auth from the path: /_ref/* uses the owner session cookie,\n`);
171
+ io.stdout.write(` /v1/owner/* uses the owner bearer (PDPP_OWNER_TOKEN or --owner-token-stdin).\n`);
172
+ io.stdout.write(` It refuses a mismatched --auth, sends bodies as JSON (so no _csrf is needed),\n`);
173
+ io.stdout.write(` and never prints the cookie or bearer.\n`);
125
174
  return 0;
126
175
  }
127
176
 
128
177
  const refDispatch = {
129
178
  login: runRefLogin,
179
+ call: runRefCall,
130
180
  run: runRefRun,
131
181
  grant: runRefGrant,
132
182
  trace: runRefTrace,
133
183
  connectors: runRefConnectors,
184
+ 'event-subscriptions': runRefEventSubscriptions,
134
185
  };
135
186
  const handler = refDispatch[refCommand];
136
187
  if (!handler) {
@@ -0,0 +1,368 @@
1
+ // `pdpp owner-agent` command surface.
2
+ //
3
+ // Subcommands:
4
+ // onboard <entrypoint-url> [--credential-file <path>] [--client-id <id>]
5
+ // Discover the trusted owner-agent onboarding profile, run browser-
6
+ // mediated owner approval, and write the issued owner-agent credential to
7
+ // a local file with 0600 permissions. The bearer is never printed.
8
+ // status [--credential-file <path>] [--entrypoint <url>]
9
+ // Introspect the stored credential and print only non-secret status.
10
+ // control [--credential-file <path>] [--entrypoint <url>]
11
+ // Discover non-secret owner-agent control capabilities (the
12
+ // GET /v1/owner/control capability document) and list configured
13
+ // connection instances (GET /v1/owner/connections) with their
14
+ // connection_id, connector identity, and label/label-needed state. The
15
+ // bearer is sent as an Authorization header and never printed.
16
+ // revoke [--credential-file <path>] [--entrypoint <url>]
17
+ // Revoke the stored credential via owner-session-gated RFC 7592 client
18
+ // delete. Uses the cached `pdpp ref login` owner session when present.
19
+ //
20
+ // This command is owner-level local automation, distinct from the grant-scoped
21
+ // `pdpp connect` path. It must never present owner bearers as the default path
22
+ // for ordinary agents or external MCP clients.
23
+
24
+ import { parseArgs } from '../ref/args.js';
25
+ import { ownerSessionHeaders } from '../ref/fetch.js';
26
+
27
+ import { resolveCredentialFile, writeOwnerAgentCredential, buildCredentialRecord } from './credential-store.js';
28
+ import { discoverOwnerAgentControl, formatOwnerAgentControl } from './control.js';
29
+ import {
30
+ findConnectorTemplates,
31
+ formatConnectionSetupPlan,
32
+ formatConnectorTemplateExplain,
33
+ formatConnectorTemplates,
34
+ requestConnectionSetupPlan,
35
+ requestConnectorTemplates,
36
+ } from './setup.js';
37
+ import { discoverOwnerAgentProfile, normalizeEntrypointUrl } from './discovery.js';
38
+ import { initiateDeviceAuthorization, pollForOwnerAgentToken } from './device-flow.js';
39
+ import { OwnerAgentError } from './errors.js';
40
+ import { introspectOwnerAgentCredential, readCredentialRecord, revokeOwnerAgentCredential } from './lifecycle.js';
41
+
42
+ const USAGE = `Trusted owner-agent onboarding (owner-level local automation):
43
+ pdpp owner-agent onboard <entrypoint-url> [--credential-file <path>] [--client-id <id>] [--client-name <name>]
44
+ pdpp owner-agent status [--credential-file <path>] [--entrypoint <url>]
45
+ pdpp owner-agent control [--credential-file <path>] [--entrypoint <url>]
46
+ pdpp owner-agent connectors list|search <query>|explain <connector-id> [--credential-file <path>] [--entrypoint <url>]
47
+ pdpp owner-agent setup <connector-id> [--display-name <name>] [--credential-file <path>] [--entrypoint <url>]
48
+ pdpp owner-agent revoke [--credential-file <path>] [--entrypoint <url>] [--cache-root <dir>] [--owner-session <cookie>]
49
+
50
+ Notes:
51
+ This is a deliberate local-admin mode, not the default agent path. Ordinary
52
+ agents should use grant-scoped access (pdpp connect). The issued bearer is
53
+ written to a local file with 0600 permissions and is never printed.
54
+ "control" lists non-secret control capabilities and configured connections
55
+ (connection_id, connector, label/label-needed); it never prints the bearer.
56
+ "connectors" lists/searches/explains available source setup options from the
57
+ non-secret connector-template catalog. It is read-only and does not mint
58
+ enrollment codes or provider credentials.
59
+ "setup" requests the same non-secret connection setup plan and next-step
60
+ contract the console and owner-agent REST surface, from the shared server
61
+ planner (POST /v1/owner/connections/intents); it never prints the bearer and
62
+ never returns provider secrets.
63
+ Revocation uses the owner-session-gated dashboard/RFC 7592 path; run
64
+ "pdpp ref login <authorization-server>" first if no owner session is cached.
65
+ Daisy's first supported target: ~/applications/daisy/.pi/agent/pdpp-owner-agent.json`;
66
+
67
+ export async function runOwnerAgent(argv, io = {}, deps = {}) {
68
+ const out = io.stdout ?? process.stdout;
69
+ const err = io.stderr ?? process.stderr;
70
+ const [subcommand, ...rest] = argv;
71
+
72
+ if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
73
+ out.write(`${USAGE}\n`);
74
+ return 0;
75
+ }
76
+
77
+ try {
78
+ if (subcommand === 'onboard') {
79
+ return await runOnboard(rest, { out }, deps);
80
+ }
81
+ if (subcommand === 'status') {
82
+ return await runStatus(rest, { out }, deps);
83
+ }
84
+ if (subcommand === 'control') {
85
+ return await runControl(rest, { out }, deps);
86
+ }
87
+ if (subcommand === 'connectors') {
88
+ return await runConnectors(rest, { out }, deps);
89
+ }
90
+ if (subcommand === 'setup') {
91
+ return await runSetup(rest, { out }, deps);
92
+ }
93
+ if (subcommand === 'revoke') {
94
+ return await runRevoke(rest, { out }, deps);
95
+ }
96
+ err.write(`Unknown owner-agent command: ${subcommand}\n\n${USAGE}\n`);
97
+ return 64;
98
+ } catch (error) {
99
+ if (error instanceof OwnerAgentError) {
100
+ err.write(`${error.message}\n`);
101
+ return error.exitCode;
102
+ }
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ async function runConnectors(argv, { out }, deps) {
108
+ const { record, positionals } = await loadRecord(argv, deps);
109
+ const fetchFn = deps.fetch ?? globalThis.fetch;
110
+ const action = positionals[0] ?? 'list';
111
+ const templates = await requestConnectorTemplates({ fetchFn, record });
112
+
113
+ if (action === 'list') {
114
+ out.write(formatConnectorTemplates(templates));
115
+ return 0;
116
+ }
117
+ if (action === 'search') {
118
+ const query = positionals.slice(1).join(' ').trim();
119
+ if (!query) {
120
+ throw new OwnerAgentError(
121
+ 'invalid_request',
122
+ 'Usage: pdpp owner-agent connectors search <query> [--credential-file <path>] [--entrypoint <url>]',
123
+ 64
124
+ );
125
+ }
126
+ out.write(formatConnectorTemplates(templates, { query }));
127
+ return 0;
128
+ }
129
+ if (action === 'explain') {
130
+ const connectorId = positionals[1];
131
+ if (typeof connectorId !== 'string' || !connectorId.trim()) {
132
+ throw new OwnerAgentError(
133
+ 'invalid_request',
134
+ 'Usage: pdpp owner-agent connectors explain <connector-id> [--credential-file <path>] [--entrypoint <url>]',
135
+ 64
136
+ );
137
+ }
138
+ const matches = findConnectorTemplates(templates, connectorId);
139
+ const exact = matches.find((template) => {
140
+ const key = template?.connector_key ?? template?.connector_id;
141
+ return typeof key === 'string' && key.toLowerCase() === connectorId.trim().toLowerCase();
142
+ });
143
+ out.write(formatConnectorTemplateExplain(exact ?? matches[0] ?? null));
144
+ return 0;
145
+ }
146
+
147
+ throw new OwnerAgentError(
148
+ 'invalid_request',
149
+ `Unknown owner-agent connectors command: ${action}\n\n${USAGE}`,
150
+ 64
151
+ );
152
+ }
153
+
154
+ async function runOnboard(argv, { out }, deps) {
155
+ const { flags, positionals } = parseArgs(argv);
156
+ const entrypoint = normalizeEntrypointUrl(positionals[0]);
157
+ if (!entrypoint) {
158
+ throw new OwnerAgentError(
159
+ 'invalid_entrypoint',
160
+ 'Usage: pdpp owner-agent onboard <entrypoint-url> [--credential-file <path>]',
161
+ 64
162
+ );
163
+ }
164
+ const fetchFn = deps.fetch ?? globalThis.fetch;
165
+ const now = deps.now ?? (() => Date.now());
166
+
167
+ const profile = await discoverOwnerAgentProfile(entrypoint, { fetch: fetchFn });
168
+
169
+ const explicitClientId = typeof flags['client-id'] === 'string' ? flags['client-id'] : undefined;
170
+ const registeredClient = explicitClientId
171
+ ? { client_id: explicitClientId, client_name: null }
172
+ : await registerOwnerAgentClient({
173
+ fetchFn,
174
+ endpoint: profile.registrationEndpoint,
175
+ clientName:
176
+ typeof flags['client-name'] === 'string' && flags['client-name'].trim()
177
+ ? flags['client-name'].trim()
178
+ : 'PDPP trusted owner agent',
179
+ });
180
+ const clientId = registeredClient.client_id;
181
+ const registrationClientUri = buildRegistrationClientUri(profile, clientId);
182
+ const device = await initiateDeviceAuthorization({
183
+ fetchFn,
184
+ endpoint: profile.deviceAuthorizationEndpoint,
185
+ clientId,
186
+ });
187
+
188
+ // Print only non-secret approval instructions.
189
+ out.write('Trusted owner-agent onboarding (owner-level local automation).\n');
190
+ out.write(`Open this URL in a browser to approve owner-agent access:\n${device.verificationUri}\n`);
191
+ if (device.userCode) {
192
+ out.write(`Verification code: ${device.userCode}\n`);
193
+ }
194
+ out.write('Waiting for owner approval...\n');
195
+
196
+ const credential = await pollForOwnerAgentToken({
197
+ fetchFn,
198
+ endpoint: profile.tokenEndpoint,
199
+ clientId,
200
+ deviceCode: device.deviceCode,
201
+ intervalMs: device.intervalMs,
202
+ timeoutMs: device.expiresInMs,
203
+ sleep: deps.sleep,
204
+ now,
205
+ onPending: deps.onPending,
206
+ });
207
+
208
+ const record = buildCredentialRecord({
209
+ resource: profile.resource,
210
+ authorizationServer: profile.authorizationServer,
211
+ credential,
212
+ clientId,
213
+ introspectionEndpoint: profile.introspectionEndpoint,
214
+ registrationEndpoint: profile.registrationEndpoint,
215
+ registrationClientUri,
216
+ schemaEndpoint: profile.schemaEndpoint,
217
+ schemaCompactEndpoint: profile.schemaCompactEndpoint,
218
+ streamsEndpoint: profile.streamsEndpoint,
219
+ createdAt: new Date(now()).toISOString(),
220
+ });
221
+
222
+ const targetPath = resolveCredentialFile({
223
+ credentialFile: typeof flags['credential-file'] === 'string' ? flags['credential-file'] : undefined,
224
+ resource: profile.resource,
225
+ home: deps.home,
226
+ });
227
+ await writeOwnerAgentCredential(targetPath, record);
228
+
229
+ // Non-secret status only. Never print credential.access_token.
230
+ out.write(`Owner-agent credential stored at ${targetPath} (mode 0600)\n`);
231
+ out.write(` token kind: ${record.pdpp_token_kind}\n`);
232
+ if (record.client_id) {
233
+ out.write(` client id: ${record.client_id}\n`);
234
+ }
235
+ out.write(` resource: ${record.resource}\n`);
236
+ if (record.expires_at) {
237
+ out.write(` expires: ${record.expires_at}\n`);
238
+ }
239
+ if (record.registration_client_uri) {
240
+ out.write(' revocation: owner-session-gated RFC 7592 client delete handle stored\n');
241
+ }
242
+ out.write('Note: /mcp rejects owner bearers; this credential is for owner-level REST/control-plane use.\n');
243
+ return 0;
244
+ }
245
+
246
+ async function runStatus(argv, { out }, deps) {
247
+ const { record } = await loadRecord(argv, deps);
248
+ const fetchFn = deps.fetch ?? globalThis.fetch;
249
+ const introspection = await introspectOwnerAgentCredential({ fetchFn, record });
250
+ out.write(`active: ${introspection.active}\n`);
251
+ if (introspection.token_kind) out.write(`token kind: ${introspection.token_kind}\n`);
252
+ if (introspection.sub) out.write(`subject: ${introspection.sub}\n`);
253
+ if (introspection.client_id) out.write(`client id: ${introspection.client_id}\n`);
254
+ if (introspection.exp) out.write(`expires (epoch): ${introspection.exp}\n`);
255
+ return introspection.active ? 0 : 1;
256
+ }
257
+
258
+ async function runControl(argv, { out }, deps) {
259
+ const { record } = await loadRecord(argv, deps);
260
+ const fetchFn = deps.fetch ?? globalThis.fetch;
261
+ const { control, connections } = await discoverOwnerAgentControl({ fetchFn, record });
262
+ out.write(formatOwnerAgentControl({ control, connections }));
263
+ return 0;
264
+ }
265
+
266
+ async function runSetup(argv, { out }, deps) {
267
+ const { record, flags, positionals } = await loadRecord(argv, deps);
268
+ const connectorId = positionals[0];
269
+ if (typeof connectorId !== 'string' || !connectorId.trim()) {
270
+ throw new OwnerAgentError(
271
+ 'invalid_request',
272
+ 'Usage: pdpp owner-agent setup <connector-id> [--display-name <name>] [--credential-file <path>] [--entrypoint <url>]',
273
+ 64
274
+ );
275
+ }
276
+ const displayName = typeof flags['display-name'] === 'string' ? flags['display-name'] : null;
277
+ const fetchFn = deps.fetch ?? globalThis.fetch;
278
+ const plan = await requestConnectionSetupPlan({ fetchFn, record, connectorId, displayName });
279
+ out.write(formatConnectionSetupPlan(plan));
280
+ return 0;
281
+ }
282
+
283
+ async function runRevoke(argv, { out }, deps) {
284
+ const { record, targetPath, flags } = await loadRecord(argv, deps);
285
+ const fetchFn = deps.fetch ?? globalThis.fetch;
286
+ const ownerSession = ownerSessionHeaders({
287
+ ownerSession: flags['owner-session'] || '',
288
+ referenceUrl: record.authorization_server,
289
+ cacheRoot: flags['cache-root'],
290
+ }).Cookie;
291
+ const result = await revokeOwnerAgentCredential({ fetchFn, record, ownerSessionCookie: ownerSession });
292
+ out.write(
293
+ result.already_absent
294
+ ? `Owner-agent credential already absent at the authorization server (${targetPath}).\n`
295
+ : `Owner-agent credential revoked (${targetPath}).\n`
296
+ );
297
+ return 0;
298
+ }
299
+
300
+ async function loadRecord(argv, deps) {
301
+ const { flags, positionals } = parseArgs(argv);
302
+ const credentialFile = typeof flags['credential-file'] === 'string' ? flags['credential-file'] : undefined;
303
+ const entrypoint = typeof flags.entrypoint === 'string' ? normalizeEntrypointUrl(flags.entrypoint) : null;
304
+ const targetPath = resolveCredentialFile({
305
+ credentialFile,
306
+ resource: entrypoint ?? 'https://owner-agent.invalid',
307
+ home: deps.home,
308
+ });
309
+ const record = await readCredentialRecord(targetPath);
310
+ return { record, targetPath, flags, positionals };
311
+ }
312
+
313
+ export { USAGE as OWNER_AGENT_USAGE };
314
+
315
+ async function registerOwnerAgentClient({ fetchFn, endpoint, clientName }) {
316
+ if (!endpoint) {
317
+ throw new OwnerAgentError(
318
+ 'registration_unavailable',
319
+ 'Owner-agent onboarding requires a registration_endpoint, or pass --client-id for an existing public client.'
320
+ );
321
+ }
322
+ let response;
323
+ try {
324
+ response = await fetchFn(endpoint, {
325
+ method: 'POST',
326
+ headers: {
327
+ Accept: 'application/json',
328
+ 'Content-Type': 'application/json',
329
+ },
330
+ body: JSON.stringify({
331
+ client_name: clientName,
332
+ token_endpoint_auth_method: 'none',
333
+ }),
334
+ });
335
+ } catch (error) {
336
+ throw new OwnerAgentError('registration_failed', `Dynamic client registration failed: ${error.message}.`);
337
+ }
338
+ let json = null;
339
+ try {
340
+ json = await response.json();
341
+ } catch {
342
+ json = null;
343
+ }
344
+ if (!response.ok) {
345
+ const code = json?.error?.code ?? json?.error ?? `http_${response.status}`;
346
+ throw new OwnerAgentError('registration_failed', `Dynamic client registration failed (${code}).`);
347
+ }
348
+ if (!json?.client_id) {
349
+ throw new OwnerAgentError('registration_invalid', 'Dynamic client registration response did not include client_id.');
350
+ }
351
+ return json;
352
+ }
353
+
354
+ function buildRegistrationClientUri(profile, clientId) {
355
+ if (!(profile && clientId)) {
356
+ return null;
357
+ }
358
+ if (profile.revocationPathTemplate) {
359
+ return profile.revocationPathTemplate.replace('{client_id}', encodeURIComponent(clientId));
360
+ }
361
+ if (profile.registrationEndpoint) {
362
+ const base = profile.registrationEndpoint.endsWith('/')
363
+ ? profile.registrationEndpoint
364
+ : `${profile.registrationEndpoint}/`;
365
+ return `${base}${encodeURIComponent(clientId)}`;
366
+ }
367
+ return null;
368
+ }