@pdpp/cli 0.1.0-beta.7 → 0.1.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/README.md +51 -13
- package/package.json +2 -2
- package/src/collector/commands.js +5 -6
- package/src/collector/runner.js +3 -3
- package/src/index.js +54 -3
- package/src/owner-agent/command.js +368 -0
- package/src/owner-agent/control.js +138 -0
- package/src/owner-agent/credential-store.js +126 -0
- package/src/owner-agent/device-flow.js +145 -0
- package/src/owner-agent/discovery.js +233 -0
- package/src/owner-agent/errors.js +13 -0
- package/src/owner-agent/lifecycle.js +126 -0
- package/src/owner-agent/setup.js +378 -0
- package/src/package-info.d.ts +2 -2
- package/src/package-info.js +4 -2
- package/src/read/commands.js +250 -0
- package/src/ref/auth.js +179 -0
- package/src/ref/commands/call.js +168 -0
- package/src/ref/commands/connectors.js +44 -4
- package/src/ref/commands/event-subscriptions.js +190 -0
- package/src/ref/commands/grant.js +3 -1
- package/src/ref/commands/run.js +3 -1
- package/src/ref/commands/trace.js +3 -1
- package/src/ref/output.js +44 -0
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ Command-line tools for PDPP providers.
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
This package is the public npm home for the `pdpp` command. The
|
|
8
|
-
supports
|
|
7
|
+
This package is the public npm home for the `pdpp` command. The CLI
|
|
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
|
|
@@ -20,9 +61,9 @@ supports three command namespaces:
|
|
|
20
61
|
cannot run on its own. The runner ships separately as
|
|
21
62
|
`@pdpp/local-collector` and owns the `pdpp-local-collector` binary; `pdpp
|
|
22
63
|
collector ...` is a slim `@pdpp/cli` shim that resolves that package lazily.
|
|
23
|
-
Public onboarding should use `npx -y @pdpp/local-collector
|
|
24
|
-
`npm i -g @pdpp/local-collector
|
|
25
|
-
|
|
64
|
+
Public onboarding should use `npx -y @pdpp/local-collector ...` or
|
|
65
|
+
`npm i -g @pdpp/local-collector`, unless the operator intentionally wants
|
|
66
|
+
the `@pdpp/cli` shim.
|
|
26
67
|
|
|
27
68
|
- **`pdpp ref ...`** — reference operator diagnostics over `_ref` routes on a
|
|
28
69
|
running reference deployment. Current subcommands: `pdpp ref run timeline
|
|
@@ -34,12 +75,9 @@ supports three command namespaces:
|
|
|
34
75
|
|
|
35
76
|
```bash
|
|
36
77
|
# @pdpp/cli package, npx-launched pdpp binary
|
|
37
|
-
npx -y @pdpp/cli
|
|
78
|
+
npx -y @pdpp/cli --help
|
|
38
79
|
```
|
|
39
80
|
|
|
40
|
-
Use the `beta` dist-tag until PDPP intentionally enables stable `latest`
|
|
41
|
-
publication.
|
|
42
|
-
|
|
43
81
|
When working from this monorepo without installing or linking the binary, use
|
|
44
82
|
the workspace executable:
|
|
45
83
|
|
|
@@ -54,11 +92,11 @@ local workspace launcher.
|
|
|
54
92
|
The local collector runtime is a separate public package:
|
|
55
93
|
|
|
56
94
|
```bash
|
|
57
|
-
# @pdpp/local-collector
|
|
58
|
-
npx -y @pdpp/local-collector
|
|
95
|
+
# @pdpp/local-collector package, npx-launched pdpp-local-collector binary
|
|
96
|
+
npx -y @pdpp/local-collector advertise
|
|
59
97
|
|
|
60
|
-
# @pdpp/local-collector
|
|
61
|
-
npm i -g @pdpp/local-collector
|
|
98
|
+
# @pdpp/local-collector package, installs the pdpp-local-collector binary
|
|
99
|
+
npm i -g @pdpp/local-collector
|
|
62
100
|
pdpp-local-collector advertise
|
|
63
101
|
```
|
|
64
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdpp/cli",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Command-line tools for PDPP providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"access": "public",
|
|
45
45
|
"provenance": false,
|
|
46
46
|
"registry": "https://registry.npmjs.org/",
|
|
47
|
-
"tag": "
|
|
47
|
+
"tag": "latest"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -12,12 +12,11 @@ cannot run on its own. Runner-owned flags are documented by
|
|
|
12
12
|
Distribution:
|
|
13
13
|
The collector runtime ships in @pdpp/local-collector, a separate npm
|
|
14
14
|
package owned by the PDPP monorepo. @pdpp/cli stays slim and resolves
|
|
15
|
-
the runner lazily.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
npx -y @pdpp/local-collector@beta advertise
|
|
15
|
+
the runner lazily. Install or run it from npm:
|
|
16
|
+
# @pdpp/local-collector package, installs pdpp-local-collector
|
|
17
|
+
npm i -g @pdpp/local-collector
|
|
18
|
+
# @pdpp/local-collector package, npx-launched pdpp-local-collector
|
|
19
|
+
npx -y @pdpp/local-collector advertise
|
|
21
20
|
See openspec/changes/publish-pdpp-local-collector/design.md.
|
|
22
21
|
|
|
23
22
|
Usage:
|
package/src/collector/runner.js
CHANGED
|
@@ -10,7 +10,7 @@ import { CollectorUsageError } from './errors.js';
|
|
|
10
10
|
* Resolve the published `@pdpp/local-collector` package, if installed.
|
|
11
11
|
*
|
|
12
12
|
* The shim prefers an installed `@pdpp/local-collector` so an operator who
|
|
13
|
-
* `npm i -g @pdpp/cli
|
|
13
|
+
* `npm i -g @pdpp/cli && npm i -g @pdpp/local-collector` can run
|
|
14
14
|
* `pdpp collector ...` without a monorepo checkout. Resolution is lazy —
|
|
15
15
|
* the CLI does NOT declare a runtime dependency on `@pdpp/local-collector`
|
|
16
16
|
* (per `publish-pdpp-local-collector` task 4.4); a missing package is
|
|
@@ -108,12 +108,12 @@ export function resolveTsxBinary(startDir = dirname(fileURLToPath(import.meta.ur
|
|
|
108
108
|
*/
|
|
109
109
|
const RUNNER_MISSING_MESSAGE =
|
|
110
110
|
'pdpp collector requires @pdpp/local-collector. Install once with ' +
|
|
111
|
-
'"npm i -g @pdpp/local-collector
|
|
111
|
+
'"npm i -g @pdpp/local-collector" or run "npx -y @pdpp/local-collector ...". ' +
|
|
112
112
|
'See openspec/changes/publish-pdpp-local-collector/design.md.';
|
|
113
113
|
|
|
114
114
|
const TSX_MISSING_MESSAGE =
|
|
115
115
|
'Could not locate tsx alongside the collector runner. Install ' +
|
|
116
|
-
'@pdpp/local-collector with "npm i -g @pdpp/local-collector
|
|
116
|
+
'@pdpp/local-collector with "npm i -g @pdpp/local-collector" or run ' +
|
|
117
117
|
'"pnpm install" at the monorepo root.';
|
|
118
118
|
|
|
119
119
|
/**
|
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,21 +42,30 @@ 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.
|
|
41
|
-
"pdpp collector" is a thin @pdpp/cli shim. Install @pdpp/local-collector
|
|
42
|
-
once, or use "npx -y @pdpp/local-collector
|
|
43
|
-
collectors like Claude Code and Codex
|
|
57
|
+
"pdpp collector" is a thin @pdpp/cli shim. Install @pdpp/local-collector
|
|
58
|
+
once, or use "npx -y @pdpp/local-collector ..." directly, for filesystem
|
|
59
|
+
collectors like Claude Code and Codex.
|
|
44
60
|
"pdpp ref" commands require a running PDPP reference server and an owner session.
|
|
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
|
+
}
|