teamclaws 1.0.4 → 2.0.1

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/pairing.js CHANGED
@@ -4,6 +4,7 @@ import { PAIRING_STATUS_TIMEOUT_MS } from "./constants.js";
4
4
  import { errorFromResponse, requestJson, } from "./http.js";
5
5
  import { directTargetUrl, isObject, parsePairingCredential, parseVerifiedDevice, parseVerifiedWorkspace, requireInteger, } from "./validation.js";
6
6
  import { openExternalBrowser } from "./browser.js";
7
+ import { TargetAuthorization, requestTargetJson, } from "./target-authorization.js";
7
8
  export class PairingClient {
8
9
  fetcher;
9
10
  store;
@@ -11,6 +12,7 @@ export class PairingClient {
11
12
  openBrowser;
12
13
  now;
13
14
  wait;
15
+ authorization;
14
16
  constructor(options) {
15
17
  this.fetcher = options.fetcher ?? fetch;
16
18
  this.store = options.store;
@@ -18,13 +20,18 @@ export class PairingClient {
18
20
  this.openBrowser = options.openBrowser ?? openExternalBrowser;
19
21
  this.now = options.now ?? Date.now;
20
22
  this.wait = options.wait ?? waitFor;
23
+ this.authorization =
24
+ options.authorization ??
25
+ new TargetAuthorization(this.store, this.fetcher);
21
26
  }
22
27
  async status(target, signal) {
23
28
  let credential;
24
29
  try {
25
30
  credential = await this.store.findWorkspace(target);
26
31
  }
27
- catch {
32
+ catch (error) {
33
+ if (error instanceof TeamClawsError && error.code === "re_pair_required")
34
+ throw error;
28
35
  this.output.pairingStatus("unreachable");
29
36
  return "unreachable";
30
37
  }
@@ -38,10 +45,8 @@ export class PairingClient {
38
45
  display_name: credential.display_name,
39
46
  };
40
47
  try {
41
- const response = await requestJson(this.fetcher, `${credential.origin}/teamclaws-admin/api/device-pairings/me/`, {
42
- headers: {
43
- Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
44
- },
48
+ const url = `${credential.origin}/teamclaws-admin/api/device-pairings/me/`;
49
+ const response = await requestTargetJson(this.fetcher, this.authorization, url, credential, {
45
50
  timeoutMs: PAIRING_STATUS_TIMEOUT_MS,
46
51
  ...(signal ? { signal } : {}),
47
52
  });
@@ -63,6 +68,9 @@ export class PairingClient {
63
68
  catch (error) {
64
69
  if (signal?.aborted)
65
70
  throw error;
71
+ if (error instanceof TeamClawsError &&
72
+ !["network_error", "network_timeout"].includes(error.code))
73
+ throw error;
66
74
  this.output.pairingStatus("unreachable", workspace);
67
75
  return "unreachable";
68
76
  }
@@ -74,50 +82,52 @@ export class PairingClient {
74
82
  let credential;
75
83
  try {
76
84
  credential = await this.poll(pending, signal);
85
+ const grant = await this.authorization.requireGrant();
86
+ if (credential.pairing_id !== credential.device_id ||
87
+ credential.server_id !== pending.server_id ||
88
+ credential.origin !== pending.target.origin ||
89
+ credential.requester_sub !== grant.sub ||
90
+ credential.authorization_id !== grant.authorization_id ||
91
+ credential.machine_jkt !== grant.machine_key.jkt)
92
+ throw verificationError();
77
93
  const initial = {
78
- version: 1,
94
+ version: 2,
79
95
  server_id: pending.server_id,
80
96
  origin: pending.target.origin,
81
97
  display_name: deviceName,
82
98
  device_id: credential.device_id,
99
+ pairing_id: credential.pairing_id,
83
100
  device_token: credential.device_token,
101
+ requester_sub: credential.requester_sub,
102
+ authorization_id: credential.authorization_id,
103
+ machine_jkt: credential.machine_jkt,
84
104
  created_at: new Date(this.now()).toISOString(),
85
105
  };
86
- await this.store.writeWorkspace(initial);
87
- try {
88
- const stored = await this.store.readWorkspace(pending.server_id);
89
- if (!stored)
90
- throw verificationError();
91
- const verification = await this.verify(pending, stored, signal);
92
- const verified = {
93
- ...stored,
94
- display_name: verification.display_name,
95
- };
96
- await this.store.writeWorkspace(verified);
97
- let snapshot;
98
- let generatedAt;
99
- if (Object.hasOwn(credential, "external_chat_capabilities")) {
100
- snapshot = null;
101
- generatedAt = null;
102
- try {
103
- const timestamp = credential.external_chat_capabilities_generated_at;
104
- if (typeof timestamp === "string" &&
105
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|\+00:00)$/.test(timestamp) &&
106
- Number.isFinite(Date.parse(timestamp))) {
107
- snapshot = parseCapabilityInventory(credential.external_chat_capabilities);
108
- generatedAt = timestamp;
109
- }
110
- }
111
- catch {
112
- // Optional inventory must not invalidate a successfully verified credential.
106
+ const verification = await this.verify(pending, initial, signal);
107
+ const verified = {
108
+ ...initial,
109
+ display_name: verification.display_name,
110
+ };
111
+ await this.store.writeWorkspace(verified);
112
+ let snapshot;
113
+ let generatedAt;
114
+ if (Object.hasOwn(credential, "external_chat_capabilities")) {
115
+ snapshot = null;
116
+ generatedAt = null;
117
+ try {
118
+ const timestamp = credential.external_chat_capabilities_generated_at;
119
+ if (typeof timestamp === "string" &&
120
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|\+00:00)$/.test(timestamp) &&
121
+ Number.isFinite(Date.parse(timestamp))) {
122
+ snapshot = parseCapabilityInventory(credential.external_chat_capabilities);
123
+ generatedAt = timestamp;
113
124
  }
114
125
  }
115
- this.output.pairingComplete(pending.server_id, pending.target.origin, verification.display_name, verification.external_chat_handoff, snapshot, generatedAt);
116
- }
117
- catch (error) {
118
- await this.store.removeWorkspace(pending.server_id);
119
- throw error;
126
+ catch {
127
+ // Optional inventory must not invalidate a successfully verified credential.
128
+ }
120
129
  }
130
+ this.output.pairingComplete(pending.server_id, pending.target.origin, verification.display_name, verification.external_chat_handoff, snapshot, generatedAt);
121
131
  }
122
132
  catch (error) {
123
133
  if (signal?.aborted) {
@@ -173,10 +183,8 @@ export class PairingClient {
173
183
  throw errorFromResponse(response, "cancel_failed");
174
184
  }
175
185
  async verify(pending, credential, signal) {
176
- const response = await requestJson(this.fetcher, directTargetUrl(pending.target, pending.target.credential_verification_path), {
177
- headers: {
178
- Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
179
- },
186
+ const url = directTargetUrl(pending.target, pending.target.credential_verification_path);
187
+ const response = await requestTargetJson(this.fetcher, this.authorization, url, credential, {
180
188
  ...(signal ? { signal } : {}),
181
189
  });
182
190
  if (response.status < 200 || response.status >= 300)
package/dist/shell.js CHANGED
@@ -2,18 +2,17 @@ import { request as httpRequest } from "node:http";
2
2
  import { request as httpsRequest } from "node:https";
3
3
  import { Readable } from "node:stream";
4
4
  import { TeamClawsError } from "./errors.js";
5
- import { errorFromResponse, requestJson } from "./http.js";
5
+ import { errorFromResponse } from "./http.js";
6
+ import { requestTargetJson } from "./target-authorization.js";
6
7
  import { isObject } from "./validation.js";
7
- export async function runShell(store, fetcher, target, command, signal) {
8
+ export async function runShell(store, fetcher, target, command, authorization, signal) {
8
9
  const credential = await store.findWorkspace(target);
9
10
  if (!credential) {
10
11
  throw new TeamClawsError("re_pair_required", "No saved pairing was found for this workspace. Pair it first.", { exitCode: 3 });
11
12
  }
12
- const response = await requestJson(fetcher, `${credential.origin}/teamclaws-admin/api/shell/`, {
13
+ const url = `${credential.origin}/teamclaws-admin/api/shell/`;
14
+ const response = await requestTargetJson(fetcher, authorization, url, credential, {
13
15
  method: "POST",
14
- headers: {
15
- Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
16
- },
17
16
  textBody: command,
18
17
  timeoutMs: null,
19
18
  responseLimitBytes: null,
package/dist/storage.js CHANGED
@@ -5,6 +5,7 @@ import { homedir, tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { TeamClawsError } from "./errors.js";
8
+ import { parseDurableAuthorization, } from "./target-authorization.js";
8
9
  import { isObject, requireHttpsUrl, requireInteger, normalizeUuid, parseLoginCredential, parseStatusWorkspaceCredential, parseWorkspaceCredential, } from "./validation.js";
9
10
  const execFile = promisify(execFileCallback);
10
11
  const SYSTEM_SID = "S-1-5-18";
@@ -13,11 +14,13 @@ const UUID_CREDENTIAL_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0
13
14
  export class CredentialStore {
14
15
  root;
15
16
  loginPath;
17
+ authorizationPath;
16
18
  workspaceDirectory;
17
19
  platform;
18
20
  constructor(options = {}) {
19
21
  this.root = options.root ?? defaultRoot();
20
22
  this.loginPath = join(this.root, "cli-auth.json");
23
+ this.authorizationPath = join(this.root, "cli-authorization.json");
21
24
  this.workspaceDirectory = join(this.root, "clawgate-devices");
22
25
  this.platform = options.platform ?? process.platform;
23
26
  }
@@ -34,6 +37,19 @@ export class CredentialStore {
34
37
  throw storageError();
35
38
  });
36
39
  }
40
+ async readAuthorization() {
41
+ const value = await this.readJsonIfPresent(this.authorizationPath);
42
+ return value === undefined ? undefined : parseDurableAuthorization(value);
43
+ }
44
+ async writeAuthorization(grant) {
45
+ await this.writePrivateJson(this.authorizationPath, parseDurableAuthorization(grant));
46
+ }
47
+ async removeAuthorization() {
48
+ await unlink(this.authorizationPath).catch((error) => {
49
+ if (error.code !== "ENOENT")
50
+ throw storageError();
51
+ });
52
+ }
37
53
  workspacePath(serverId) {
38
54
  return join(this.workspaceDirectory, `${normalizeUuid(serverId)}.json`);
39
55
  }
@@ -0,0 +1,150 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { API_V2_BASE, OAUTH_TOKEN_URL } from "./constants.js";
3
+ import { TeamClawsError } from "./errors.js";
4
+ import { errorFromResponse, requestJson, } from "./http.js";
5
+ import { OsMachineKeyProvider, jwkThumbprint, } from "./machine-key.js";
6
+ import { isObject } from "./validation.js";
7
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
8
+ export class TargetAuthorization {
9
+ fetcher;
10
+ store;
11
+ keys;
12
+ now;
13
+ constructor(store, fetcher = fetch, keys = new OsMachineKeyProvider(store.root), now = Date.now) {
14
+ this.store = store;
15
+ this.fetcher = fetcher;
16
+ this.keys = keys;
17
+ this.now = now;
18
+ }
19
+ async requireGrant() {
20
+ const grant = await this.store.readAuthorization();
21
+ if (!grant)
22
+ throw new TeamClawsError("login_required", "Approve this CLI at TeamClaws with `teamclaws login` before using a workspace.", { exitCode: 3 });
23
+ return grant;
24
+ }
25
+ async accountHeadersFor(method, url) {
26
+ return this.accountHeadersForGrant(await this.requireGrant(), method, url);
27
+ }
28
+ async accountHeadersForGrant(grant, method, url) {
29
+ if (new URL(url).origin !== new URL(API_V2_BASE).origin)
30
+ throw new TeamClawsError("invalid_target", "A TeamClaws issuer URL is required.", { exitCode: 9 });
31
+ return {
32
+ "X-TeamClaws-Authorization-Id": grant.authorization_id,
33
+ DPoP: await this.dpop(grant.machine_key, method, url),
34
+ };
35
+ }
36
+ assertPairing(grant, credential) {
37
+ if (credential.version !== 2 ||
38
+ credential.requester_sub !== grant.sub ||
39
+ credential.authorization_id !== grant.authorization_id ||
40
+ credential.machine_jkt !== grant.machine_key.jkt ||
41
+ credential.pairing_id !== credential.device_id) {
42
+ throw new TeamClawsError("re_pair_required", "This pairing is not bound to the current TeamClaws authorization. Pair this workspace again.", { exitCode: 3 });
43
+ }
44
+ }
45
+ async headersFor(method, url, credential, signal) {
46
+ const grant = await this.requireGrant();
47
+ this.assertPairing(grant, credential);
48
+ if (new URL(url).origin !== credential.origin)
49
+ throw new TeamClawsError("target_mismatch", "The request target does not match the selected pairing.", { exitCode: 9 });
50
+ const pairingAuthorization = `Bearer ${credential.device_id}:${credential.device_token}`;
51
+ const exchangeUrl = `${API_V2_BASE}authorizations/${grant.authorization_id}/proofs/`;
52
+ const exchange = await requestJson(this.fetcher, exchangeUrl, {
53
+ method: "POST",
54
+ headers: {
55
+ DPoP: await this.dpop(grant.machine_key, "POST", exchangeUrl),
56
+ },
57
+ body: {
58
+ server_id: credential.server_id,
59
+ pairing_id: credential.pairing_id,
60
+ scope: "paired:invoke",
61
+ },
62
+ ...(signal ? { signal } : {}),
63
+ });
64
+ if (exchange.status < 200 || exchange.status >= 300)
65
+ throw errorFromResponse(exchange, "target_proof_denied");
66
+ if (!isObject(exchange.body) ||
67
+ exchange.body.token_type !== "DPoP" ||
68
+ exchange.body.scope !== "paired:invoke" ||
69
+ typeof exchange.body.access_token !== "string" ||
70
+ exchange.body.access_token.length < 16 ||
71
+ exchange.body.access_token.length > 8192 ||
72
+ !Number.isInteger(exchange.body.expires_in) ||
73
+ exchange.body.expires_in < 1 ||
74
+ exchange.body.expires_in > 300)
75
+ throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid target proof.", { exitCode: 9 });
76
+ const proof = exchange.body.access_token;
77
+ return {
78
+ Authorization: pairingAuthorization,
79
+ "X-TeamClaws-Target-Proof": proof,
80
+ DPoP: await this.dpop(grant.machine_key, method, url, proof, pairingAuthorization),
81
+ };
82
+ }
83
+ async dpop(key, method, url, targetProof, pairingAuthorization) {
84
+ const parsed = new URL(url);
85
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password)
86
+ throw new TeamClawsError("invalid_target", "A canonical HTTPS target is required.", { exitCode: 9 });
87
+ const header = { typ: "dpop+jwt", alg: "ES256", jwk: key.public_jwk };
88
+ const payload = {
89
+ htm: method.toUpperCase(),
90
+ htu: `${parsed.origin}${parsed.pathname}`,
91
+ iat: Math.floor(this.now() / 1000),
92
+ jti: randomUUID(),
93
+ };
94
+ if (targetProof !== undefined)
95
+ payload.ath = hashAscii(targetProof);
96
+ if (pairingAuthorization !== undefined)
97
+ payload.pairing_ath = hashAscii(pairingAuthorization);
98
+ const signingInput = `${base64Json(header)}.${base64Json(payload)}`;
99
+ return `${signingInput}.${await this.keys.sign(key, signingInput)}`;
100
+ }
101
+ async tokenDpop(key) {
102
+ return this.dpop(key, "POST", OAUTH_TOKEN_URL);
103
+ }
104
+ }
105
+ export async function requestTargetJson(fetcher, authorization, url, credential, options = {}) {
106
+ const method = options.method ?? "GET";
107
+ const attempt = async () => requestJson(fetcher, url, {
108
+ ...options,
109
+ headers: {
110
+ ...options.headers,
111
+ ...(await authorization.headersFor(method, url, credential, options.signal)),
112
+ },
113
+ });
114
+ const first = await attempt();
115
+ if (first.status === 401 &&
116
+ isObject(first.body) &&
117
+ first.body.error === "target_proof_expired")
118
+ return attempt();
119
+ return first;
120
+ }
121
+ export function parseDurableAuthorization(value) {
122
+ if (!isObject(value) ||
123
+ value.version !== 2 ||
124
+ typeof value.authorization_id !== "string" ||
125
+ !uuid.test(value.authorization_id) ||
126
+ typeof value.sub !== "string" ||
127
+ value.sub.length < 1 ||
128
+ value.sub.length > 255 ||
129
+ !isObject(value.machine_key) ||
130
+ !["windows-cng", "posix-file"].includes(String(value.machine_key.provider)) ||
131
+ typeof value.machine_key.name !== "string" ||
132
+ !/^teamclaws-cli-[0-9a-f-]{36}$/u.test(value.machine_key.name) ||
133
+ !isObject(value.machine_key.public_jwk) ||
134
+ value.machine_key.public_jwk.kty !== "EC" ||
135
+ value.machine_key.public_jwk.crv !== "P-256" ||
136
+ typeof value.machine_key.public_jwk.x !== "string" ||
137
+ typeof value.machine_key.public_jwk.y !== "string" ||
138
+ typeof value.machine_key.jkt !== "string")
139
+ throw new TeamClawsError("invalid_credential_file", "The saved CLI authorization is invalid.", { exitCode: 9 });
140
+ const key = value.machine_key;
141
+ if (key.jkt !== jwkThumbprint(key.public_jwk))
142
+ throw new TeamClawsError("invalid_credential_file", "The saved CLI authorization key is invalid.", { exitCode: 9 });
143
+ return value;
144
+ }
145
+ function hashAscii(value) {
146
+ return createHash("sha256").update(value, "ascii").digest("base64url");
147
+ }
148
+ function base64Json(value) {
149
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
150
+ }
@@ -227,6 +227,12 @@ export function parsePairingCredential(value) {
227
227
  return {
228
228
  device_id: normalizeUuid(value.device_id, "device identifier"),
229
229
  device_token: requireString(value.device_token, "device token", 8_192),
230
+ pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
231
+ server_id: normalizeUuid(value.server_id, "server identifier"),
232
+ origin: canonicalWorkspaceOrigin(value.origin),
233
+ requester_sub: requireString(value.requester_sub, "requester subject", 255),
234
+ authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
235
+ machine_jkt: requireThumbprint(value.machine_jkt),
230
236
  ...(Object.hasOwn(value, "external_chat_capabilities")
231
237
  ? {
232
238
  external_chat_capabilities: value.external_chat_capabilities,
@@ -317,13 +323,13 @@ export function parseSafeUserResponse(value) {
317
323
  exitCode: 9,
318
324
  });
319
325
  }
320
- assertExactKeys(value, ["id", "email", "display_name", "expires_at", "token_expired"], [], "account response");
321
- if (value.token_expired !== false) {
326
+ assertExactKeys(value, ["id", "email", "display_name", "authorization_id", "authorization_status"], [], "account response");
327
+ if (value.authorization_status !== "active") {
322
328
  throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid account response.", {
323
329
  exitCode: 9,
324
330
  });
325
331
  }
326
- requireIsoDate(value.expires_at, "access-token expiry");
332
+ normalizeUuid(value.authorization_id, "authorization identifier");
327
333
  return {
328
334
  id: normalizeUuid(value.id, "user identifier"),
329
335
  display_name: requireString(value.display_name, "display name", 200),
@@ -405,44 +411,70 @@ export function parseLoginCredential(value) {
405
411
  export function parseWorkspaceCredential(value) {
406
412
  if (!isObject(value))
407
413
  throw invalidCredentialFile();
414
+ if (value.version === 1)
415
+ throw legacyPairingError();
408
416
  assertExactKeys(value, [
409
417
  "version",
410
418
  "server_id",
411
419
  "origin",
412
420
  "display_name",
413
421
  "device_id",
422
+ "pairing_id",
414
423
  "device_token",
424
+ "requester_sub",
425
+ "authorization_id",
426
+ "machine_jkt",
415
427
  "created_at",
416
428
  ], [], "workspace credential file");
417
- if (value.version !== 1)
429
+ if (value.version !== 2)
418
430
  throw invalidCredentialFile();
419
431
  return {
420
- version: 1,
432
+ version: 2,
421
433
  server_id: normalizeUuid(value.server_id),
422
434
  origin: canonicalWorkspaceOrigin(value.origin),
423
435
  display_name: requireString(value.display_name, "display name", 120),
424
436
  device_id: normalizeUuid(value.device_id, "device identifier"),
437
+ pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
425
438
  device_token: requireSecret(value.device_token),
439
+ requester_sub: requireString(value.requester_sub, "requester subject", 255),
440
+ authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
441
+ machine_jkt: requireThumbprint(value.machine_jkt),
426
442
  created_at: requireIsoDate(value.created_at, "credential creation date"),
427
443
  };
428
444
  }
429
445
  export function parseStatusWorkspaceCredential(value, expectedServerId) {
430
- if (!isObject(value) || value.version !== 1)
446
+ if (!isObject(value))
447
+ throw invalidCredentialFile();
448
+ if (value.version === 1)
449
+ throw legacyPairingError();
450
+ if (value.version !== 2)
431
451
  throw invalidCredentialFile();
432
452
  const serverId = normalizeUuid(value.server_id);
433
453
  if (serverId !== normalizeUuid(expectedServerId))
434
454
  throw invalidCredentialFile();
435
455
  return {
436
- version: 1,
456
+ version: 2,
437
457
  server_id: serverId,
438
458
  origin: canonicalWorkspaceOrigin(value.origin),
439
459
  display_name: typeof value.display_name === "string" && value.display_name.length > 0
440
460
  ? requireString(value.display_name, "display name", 120)
441
461
  : "Paired device",
442
462
  device_id: normalizeUuid(value.device_id, "device identifier"),
463
+ pairing_id: normalizeUuid(value.pairing_id, "pairing identifier"),
443
464
  device_token: requireSecret(value.device_token),
465
+ requester_sub: requireString(value.requester_sub, "requester subject", 255),
466
+ authorization_id: normalizeUuid(value.authorization_id, "authorization identifier"),
467
+ machine_jkt: requireThumbprint(value.machine_jkt),
444
468
  };
445
469
  }
470
+ function requireThumbprint(value) {
471
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/u.test(value))
472
+ throw invalidCredentialFile();
473
+ return value;
474
+ }
475
+ function legacyPairingError() {
476
+ return new TeamClawsError("re_pair_required", "This saved pairing predates bound CLI authorization. Pair this workspace again.", { exitCode: 3 });
477
+ }
446
478
  function requireSecret(value) {
447
479
  if (typeof value !== "string" || value.length < 8 || value.length > 8_192) {
448
480
  throw invalidCredentialFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamclaws",
3
- "version": "1.0.4",
3
+ "version": "2.0.1",
4
4
  "description": "Official TeamClaws command-line client and agent skills",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "homepage": "https://teamclaws.com",
@@ -9,6 +9,13 @@ For install-and-connect requests, follow
9
9
  [teamclaws-onboard](../teamclaws-onboard/SKILL.md) for the conversation sequence.
10
10
  A status-only request runs only `teamclaws auth status --json` and stops.
11
11
 
12
+ Before starting browser login, explain that the user will sign this computer's
13
+ TeamClaws command-line client into their TeamClaws account through the browser.
14
+ This connects the client to their account; workspace selection and browser
15
+ pairing approval come next when they are connecting a workspace. If onboarding
16
+ already explained the process, use a short transition into this step. Give the
17
+ approval link with that context, never as an unexplained authorization request.
18
+
12
19
  Use the official executable:
13
20
 
14
21
  ```text
@@ -16,22 +23,31 @@ npx teamclaws login --json
16
23
  npx teamclaws login owner@example.com --json
17
24
  npx teamclaws auth status --json
18
25
  npx teamclaws logout --json
26
+ npx teamclaws logout --revoke --json
19
27
  ```
20
28
 
21
29
  Login uses OAuth 2.0 Device Authorization Grant against www.teamclaws.com. The
22
30
  next owner-facing message must link `login_browser_required.url` with the label
23
31
  **Sign in to TeamClaws** and include `account_guidance`. Keep the command running
24
- while the user signs in; do not restart it while awaiting approval. Confirm the
25
- returned account after `login_complete` and exit 0, not after opening a browser.
32
+ while the user signs in; do not restart it while awaiting approval. Keep your
33
+ turn active and use the execution tool's wait/read operation on the same process
34
+ handle. Do not require an "I approved" or "done" message. If the execution
35
+ environment requires yielding, use a supported automatic completion callback or
36
+ wakeup that preserves the process; report a limitation if neither is possible.
37
+ Confirm the returned account after `login_complete` and exit 0, then immediately
38
+ resume the authorized onboarding or pairing task. Denial, expiry, cancellation,
39
+ and failure end the attempt; report the result without automatically restarting.
26
40
  Start without an email unless the user supplies one. Optional email is a bind
27
41
  sent to TeamClaws, not identity asserted by this process. Never transfer a
28
- model or harness token into this process. The command-line client stores its own
29
- protected login credential locally.
42
+ model or harness token into this process. The client stores an authorization ID
43
+ and protected local signing key; temporary OAuth tokens are discarded.
30
44
 
31
45
  Only treat the fixed structured events as safe. Never expose access tokens,
32
46
  refresh tokens, device codes, cookies, or authorization headers.
33
47
 
34
- Logout always removes the local command-line login credential. A
35
- `local_only_warning` status means local logout succeeded but remote revocation
36
- could not be confirmed. Completed Clawgate workspace credentials are not
37
- deleted by logout.
48
+ Ordinary logout removes the local authorization reference. Use `--revoke` only
49
+ when the user asks to revoke this CLI authorization at TeamClaws; the CLI waits
50
+ for confirmation before removing its local reference. If remote revocation is
51
+ uncertain, report that uncertainty and retain the local reference. Completed
52
+ Clawgate workspace credentials are not deleted by logout, but old pairings
53
+ without authorization binding require re-pairing.