a web app for declaring if an atproto account is automated + package for the lexicon jsr.io/@voyager/autonomy-lexicon
atprotocol lexicon

init

+20
.gitignore
··· 1 + # Deno 2 + .deno/ 3 + 4 + # NPM build output 5 + npm/ 6 + 7 + # Environment variables 8 + .env 9 + .env.local 10 + 11 + # OS 12 + .DS_Store 13 + Thumbs.db 14 + 15 + # Editor 16 + .vscode/ 17 + .idea/ 18 + *.swp 19 + *.swo 20 + *~
+48
README.md
··· 1 + # ATP Autonomy Declaration 2 + 3 + A Deno project for managing and sharing an ATProtocol lexicon with accompanying tooling. 4 + 5 + ## Project Structure 6 + 7 + - `lexicon/` - ATProtocol lexicon definition (JSON) 8 + - `mod.ts` - Main module exporting the lexicon (published to JSR) 9 + - `client/` - Single-page web form for users to add records to their PDS 10 + - `scripts/` - Utility scripts for publishing and managing the lexicon 11 + 12 + ## Usage 13 + 14 + ### Development 15 + 16 + Run the client locally: 17 + ```bash 18 + deno task dev 19 + ``` 20 + 21 + ### Publish to JSR 22 + 23 + Publish the lexicon package to JSR: 24 + ```bash 25 + deno publish 26 + ``` 27 + 28 + Or with dry-run to check first: 29 + ```bash 30 + deno publish --dry-run 31 + ``` 32 + 33 + ### Publish Lexicon to PDS 34 + 35 + Publish the lexicon to the owning PDS: 36 + ```bash 37 + deno task publish:lexicon 38 + ``` 39 + 40 + Set up your credentials first: 41 + ```bash 42 + export ATP_HANDLE=your-handle.bsky.social 43 + export ATP_PASSWORD=your-app-password 44 + ``` 45 + 46 + ## Requirements 47 + 48 + - Deno 2.0+
+115
client/app.js
··· 1 + /** 2 + * Client-side application for submitting autonomy declarations 3 + */ 4 + 5 + import { BskyAgent } from "https://esm.sh/@atproto/api@0.13.0"; 6 + 7 + const form = document.getElementById("declaration-form"); 8 + const submitBtn = document.getElementById("submit-btn"); 9 + const resultDiv = document.getElementById("result"); 10 + 11 + form.addEventListener("submit", async (e) => { 12 + e.preventDefault(); 13 + 14 + // Get form values 15 + const handle = document.getElementById("handle").value.trim(); 16 + const password = document.getElementById("password").value; 17 + 18 + const automationLevel = document.getElementById("automationLevel").value; 19 + const usesGenerativeAI = document.getElementById("usesGenerativeAI").checked; 20 + const description = document.getElementById("description").value.trim(); 21 + 22 + const responsibleType = document.getElementById("responsibleType").value; 23 + const responsibleName = document.getElementById("responsibleName").value.trim(); 24 + const responsibleContact = document.getElementById("responsibleContact").value.trim(); 25 + const responsibleDid = document.getElementById("responsibleDid").value.trim(); 26 + 27 + const disclosureUrl = document.getElementById("disclosureUrl").value.trim(); 28 + const externalServicesText = document.getElementById("externalServices").value.trim(); 29 + 30 + // Disable form during submission 31 + submitBtn.disabled = true; 32 + submitBtn.textContent = "Submitting..."; 33 + resultDiv.className = "result hidden"; 34 + 35 + try { 36 + // Create agent and login 37 + const agent = new BskyAgent({ 38 + service: "https://bsky.social" 39 + }); 40 + 41 + await agent.login({ 42 + identifier: handle, 43 + password: password 44 + }); 45 + 46 + // Build the record 47 + const record = { 48 + $type: "studio.voyager.account.autonomy", 49 + createdAt: new Date().toISOString() 50 + }; 51 + 52 + // Add optional fields 53 + if (automationLevel) { 54 + record.automationLevel = automationLevel; 55 + } 56 + 57 + if (usesGenerativeAI) { 58 + record.usesGenerativeAI = usesGenerativeAI; 59 + } 60 + 61 + if (description) { 62 + record.description = description; 63 + } 64 + 65 + // Build responsible party object if any fields are filled 66 + if (responsibleType || responsibleName || responsibleContact || responsibleDid) { 67 + record.responsibleParty = {}; 68 + if (responsibleType) record.responsibleParty.type = responsibleType; 69 + if (responsibleName) record.responsibleParty.name = responsibleName; 70 + if (responsibleContact) record.responsibleParty.contact = responsibleContact; 71 + if (responsibleDid) record.responsibleParty.did = responsibleDid; 72 + } 73 + 74 + if (disclosureUrl) { 75 + record.disclosureUrl = disclosureUrl; 76 + } 77 + 78 + // Parse external services (one per line) 79 + if (externalServicesText) { 80 + const services = externalServicesText 81 + .split('\n') 82 + .map(s => s.trim()) 83 + .filter(s => s.length > 0) 84 + .slice(0, 20); // Max 20 services 85 + if (services.length > 0) { 86 + record.externalServices = services; 87 + } 88 + } 89 + 90 + // Submit to PDS 91 + const response = await agent.api.com.atproto.repo.createRecord({ 92 + repo: agent.session?.did || "", 93 + collection: "studio.voyager.account.autonomy", 94 + record: record 95 + }); 96 + 97 + // Show success 98 + resultDiv.className = "result success"; 99 + resultDiv.textContent = `โœ… Declaration submitted successfully! URI: ${response.data.uri}`; 100 + 101 + // Clear form 102 + form.reset(); 103 + 104 + } catch (error) { 105 + // Show error 106 + resultDiv.className = "result error"; 107 + resultDiv.textContent = `โŒ Error: ${error.message}`; 108 + console.error("Submission error:", error); 109 + } finally { 110 + // Re-enable form 111 + submitBtn.disabled = false; 112 + submitBtn.textContent = "Submit Declaration"; 113 + resultDiv.classList.remove("hidden"); 114 + } 115 + });
+153
client/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Autonomy Declaration - ATProtocol</title> 7 + <link rel="stylesheet" href="styles.css"> 8 + </head> 9 + <body> 10 + <div class="container"> 11 + <header> 12 + <h1>Autonomy Declaration</h1> 13 + <p>Declare automation and AI usage for transparency and accountability</p> 14 + </header> 15 + 16 + <main> 17 + <form id="declaration-form"> 18 + <div class="form-group"> 19 + <label for="handle">Your Handle</label> 20 + <input 21 + type="text" 22 + id="handle" 23 + name="handle" 24 + placeholder="username.bsky.social" 25 + required 26 + /> 27 + <small>Your Bluesky handle or DID</small> 28 + </div> 29 + 30 + <div class="form-group"> 31 + <label for="password">App Password</label> 32 + <input 33 + type="password" 34 + id="password" 35 + name="password" 36 + placeholder="xxxx-xxxx-xxxx-xxxx" 37 + required 38 + /> 39 + <small>Generate an app password in Bluesky settings</small> 40 + </div> 41 + 42 + <div class="form-group"> 43 + <label for="automationLevel">Automation Level</label> 44 + <select id="automationLevel" name="automationLevel"> 45 + <option value="">Select level (optional)</option> 46 + <option value="human">Human - No automation</option> 47 + <option value="assisted">Assisted - Tools help but human decides</option> 48 + <option value="collaborative">Collaborative - Human and AI work together</option> 49 + <option value="automated">Automated - Primarily AI-driven</option> 50 + </select> 51 + <small>Level of automation in account management and content creation</small> 52 + </div> 53 + 54 + <div class="form-group"> 55 + <label> 56 + <input type="checkbox" id="usesGenerativeAI" name="usesGenerativeAI" /> 57 + Uses Generative AI 58 + </label> 59 + <small>Check if this account uses LLMs, image generation, etc.</small> 60 + </div> 61 + 62 + <div class="form-group"> 63 + <label for="description">Description</label> 64 + <textarea 65 + id="description" 66 + name="description" 67 + rows="4" 68 + maxlength="300" 69 + placeholder="Explain how this account is automated and what it does..." 70 + ></textarea> 71 + <small>Plain language explanation (max 300 characters)</small> 72 + </div> 73 + 74 + <fieldset class="form-group"> 75 + <legend>Responsible Party</legend> 76 + 77 + <div class="form-group"> 78 + <label for="responsibleType">Type</label> 79 + <select id="responsibleType" name="responsibleType"> 80 + <option value="">Select type (optional)</option> 81 + <option value="person">Person</option> 82 + <option value="organization">Organization</option> 83 + </select> 84 + </div> 85 + 86 + <div class="form-group"> 87 + <label for="responsibleName">Name</label> 88 + <input 89 + type="text" 90 + id="responsibleName" 91 + name="responsibleName" 92 + maxlength="100" 93 + placeholder="Name of person or organization" 94 + /> 95 + </div> 96 + 97 + <div class="form-group"> 98 + <label for="responsibleContact">Contact</label> 99 + <input 100 + type="text" 101 + id="responsibleContact" 102 + name="responsibleContact" 103 + maxlength="300" 104 + placeholder="Email, URL, handle, or DID" 105 + /> 106 + </div> 107 + 108 + <div class="form-group"> 109 + <label for="responsibleDid">DID (optional)</label> 110 + <input 111 + type="text" 112 + id="responsibleDid" 113 + name="responsibleDid" 114 + placeholder="did:plc:..." 115 + /> 116 + <small>ATProto DID if they have an identity</small> 117 + </div> 118 + </fieldset> 119 + 120 + <div class="form-group"> 121 + <label for="disclosureUrl">Disclosure URL</label> 122 + <input 123 + type="url" 124 + id="disclosureUrl" 125 + name="disclosureUrl" 126 + placeholder="https://..." 127 + /> 128 + <small>URL with additional information about automation</small> 129 + </div> 130 + 131 + <div class="form-group"> 132 + <label for="externalServices">External Services</label> 133 + <textarea 134 + id="externalServices" 135 + name="externalServices" 136 + rows="3" 137 + placeholder="Letta, Railway, Google Gemini 2.5-pro (one per line)" 138 + ></textarea> 139 + <small>External tools and services used (one per line, max 20)</small> 140 + </div> 141 + 142 + <button type="submit" id="submit-btn"> 143 + Submit Declaration 144 + </button> 145 + </form> 146 + 147 + <div id="result" class="result hidden"></div> 148 + </main> 149 + </div> 150 + 151 + <script type="module" src="./app.js"></script> 152 + </body> 153 + </html>
+17
client/main.ts
··· 1 + /** 2 + * Simple development server for the client 3 + */ 4 + 5 + import { serveDir } from "https://deno.land/std@0.224.0/http/file_server.ts"; 6 + 7 + Deno.serve({ 8 + port: 8000, 9 + handler: (req) => { 10 + return serveDir(req, { 11 + fsRoot: "./client", 12 + showDirListing: false, 13 + }); 14 + }, 15 + }); 16 + 17 + console.log("๐Ÿš€ Client running at http://localhost:8000");
+148
client/styles.css
··· 1 + * { 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 + } 6 + 7 + body { 8 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 9 + line-height: 1.6; 10 + color: #333; 11 + background: #f5f5f5; 12 + padding: 20px; 13 + } 14 + 15 + .container { 16 + max-width: 600px; 17 + margin: 0 auto; 18 + background: white; 19 + padding: 40px; 20 + border-radius: 8px; 21 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 22 + } 23 + 24 + header { 25 + text-align: center; 26 + margin-bottom: 40px; 27 + } 28 + 29 + h1 { 30 + font-size: 2rem; 31 + margin-bottom: 10px; 32 + color: #1a1a1a; 33 + } 34 + 35 + header p { 36 + color: #666; 37 + } 38 + 39 + .form-group { 40 + margin-bottom: 24px; 41 + } 42 + 43 + label { 44 + display: block; 45 + margin-bottom: 8px; 46 + font-weight: 500; 47 + color: #333; 48 + } 49 + 50 + input[type="text"], 51 + input[type="password"], 52 + input[type="url"], 53 + select, 54 + textarea { 55 + width: 100%; 56 + padding: 12px; 57 + border: 1px solid #ddd; 58 + border-radius: 4px; 59 + font-size: 14px; 60 + font-family: inherit; 61 + transition: border-color 0.2s; 62 + } 63 + 64 + input[type="text"]:focus, 65 + input[type="password"]:focus, 66 + input[type="url"]:focus, 67 + select:focus, 68 + textarea:focus { 69 + outline: none; 70 + border-color: #0085ff; 71 + } 72 + 73 + select { 74 + cursor: pointer; 75 + } 76 + 77 + textarea { 78 + resize: vertical; 79 + min-height: 100px; 80 + } 81 + 82 + input[type="checkbox"] { 83 + margin-right: 8px; 84 + } 85 + 86 + fieldset { 87 + border: 1px solid #ddd; 88 + border-radius: 4px; 89 + padding: 20px; 90 + margin-bottom: 24px; 91 + } 92 + 93 + legend { 94 + font-weight: 600; 95 + color: #333; 96 + padding: 0 8px; 97 + } 98 + 99 + small { 100 + display: block; 101 + margin-top: 4px; 102 + color: #666; 103 + font-size: 12px; 104 + } 105 + 106 + button[type="submit"] { 107 + width: 100%; 108 + padding: 14px; 109 + background: #0085ff; 110 + color: white; 111 + border: none; 112 + border-radius: 4px; 113 + font-size: 16px; 114 + font-weight: 500; 115 + cursor: pointer; 116 + transition: background 0.2s; 117 + } 118 + 119 + button[type="submit"]:hover { 120 + background: #0070dd; 121 + } 122 + 123 + button[type="submit"]:disabled { 124 + background: #ccc; 125 + cursor: not-allowed; 126 + } 127 + 128 + .result { 129 + margin-top: 24px; 130 + padding: 16px; 131 + border-radius: 4px; 132 + } 133 + 134 + .result.hidden { 135 + display: none; 136 + } 137 + 138 + .result.success { 139 + background: #d4edda; 140 + color: #155724; 141 + border: 1px solid #c3e6cb; 142 + } 143 + 144 + .result.error { 145 + background: #f8d7da; 146 + color: #721c24; 147 + border: 1px solid #f5c6cb; 148 + }
+20
deno.json
··· 1 + { 2 + "name": "@voyager/autonomy-lexicon", 3 + "version": "0.1.0", 4 + "exports": "./mod.ts", 5 + "tasks": { 6 + "dev": "deno run --watch --allow-net --allow-read client/main.ts", 7 + "publish:lexicon": "deno run --env --allow-net --allow-read --allow-env scripts/publish_lexicon.ts" 8 + }, 9 + "imports": { 10 + "@atproto/api": "npm:@atproto/api@^0.13.0" 11 + }, 12 + "publish": { 13 + "exclude": [ 14 + "client/", 15 + "scripts/", 16 + ".env", 17 + ".env.example" 18 + ] 19 + } 20 + }
+83
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:@atproto/api@0.13": "0.13.35" 5 + }, 6 + "npm": { 7 + "@atproto/api@0.13.35": { 8 + "integrity": "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g==", 9 + "dependencies": [ 10 + "@atproto/common-web", 11 + "@atproto/lexicon", 12 + "@atproto/syntax@0.3.4", 13 + "@atproto/xrpc", 14 + "await-lock", 15 + "multiformats", 16 + "tlds", 17 + "zod" 18 + ] 19 + }, 20 + "@atproto/common-web@0.4.3": { 21 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 22 + "dependencies": [ 23 + "graphemer", 24 + "multiformats", 25 + "uint8arrays", 26 + "zod" 27 + ] 28 + }, 29 + "@atproto/lexicon@0.4.14": { 30 + "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", 31 + "dependencies": [ 32 + "@atproto/common-web", 33 + "@atproto/syntax@0.4.1", 34 + "iso-datestring-validator", 35 + "multiformats", 36 + "zod" 37 + ] 38 + }, 39 + "@atproto/syntax@0.3.4": { 40 + "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==" 41 + }, 42 + "@atproto/syntax@0.4.1": { 43 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==" 44 + }, 45 + "@atproto/xrpc@0.6.12": { 46 + "integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==", 47 + "dependencies": [ 48 + "@atproto/lexicon", 49 + "zod" 50 + ] 51 + }, 52 + "await-lock@2.2.2": { 53 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" 54 + }, 55 + "graphemer@1.4.0": { 56 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 57 + }, 58 + "iso-datestring-validator@2.2.2": { 59 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 60 + }, 61 + "multiformats@9.9.0": { 62 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 63 + }, 64 + "tlds@1.260.0": { 65 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 66 + "bin": true 67 + }, 68 + "uint8arrays@3.0.0": { 69 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 70 + "dependencies": [ 71 + "multiformats" 72 + ] 73 + }, 74 + "zod@3.25.76": { 75 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 76 + } 77 + }, 78 + "workspace": { 79 + "dependencies": [ 80 + "npm:@atproto/api@0.13" 81 + ] 82 + } 83 + }
+9
jsr.json
··· 1 + { 2 + "name": "@voyager/autonomy-lexicon", 3 + "version": "0.1.0", 4 + "license": "MIT", 5 + "exports": "./mod.ts", 6 + "publish": { 7 + "exclude": ["client/", "scripts/", ".env", ".env.example", "npm/"] 8 + } 9 + }
+86
lexicon/autonomy-declaration.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "studio.voyager.account.autonomy", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "automationLevel": { 12 + "type": "string", 13 + "knownValues": [ 14 + "human", 15 + "assisted", 16 + "collaborative", 17 + "automated" 18 + ], 19 + "description": "Level of automation in account management and content creation" 20 + }, 21 + "usesGenerativeAI": { 22 + "type": "boolean", 23 + "description": "Whether this account uses generative AI (LLMs, image generation, etc.) to create content" 24 + }, 25 + "description": { 26 + "type": "string", 27 + "maxGraphemes": 300, 28 + "description": "Plain language explanation of how this account is automated and what it does" 29 + }, 30 + "responsibleParty": { 31 + "type": "object", 32 + "properties": { 33 + "type": { 34 + "type": "string", 35 + "knownValues": [ 36 + "person", 37 + "organization" 38 + ], 39 + "description": "Whether the responsible party is a person or organization" 40 + }, 41 + "name": { 42 + "type": "string", 43 + "maxGraphemes": 100, 44 + "description": "Name of the person or organization responsible" 45 + }, 46 + "contact": { 47 + "type": "string", 48 + "maxLength": 300, 49 + "description": "Contact information (email, URL, handle, or DID)" 50 + }, 51 + "did": { 52 + "type": "string", 53 + "format": "did", 54 + "description": "DID of the responsible party if they have an ATProto identity" 55 + } 56 + }, 57 + "description": "Information about who is accountable for this account's automated behavior" 58 + }, 59 + "disclosureUrl": { 60 + "type": "string", 61 + "format": "uri", 62 + "description": "URL with additional information about this account's automation" 63 + }, 64 + "externalServices": { 65 + "type": "array", 66 + "items": { 67 + "type": "string", 68 + "maxLength": 200 69 + }, 70 + "maxLength": 20, 71 + "description": "External tools and services this agent relies on outside of Bluesky (e.g., 'Letta', 'Railway', 'Google Gemini 2.5-pro')" 72 + }, 73 + "createdAt": { 74 + "type": "string", 75 + "format": "datetime", 76 + "description": "Timestamp when this declaration was created" 77 + } 78 + }, 79 + "required": [ 80 + "createdAt" 81 + ] 82 + }, 83 + "description": "Declaration of automation and AI usage for transparency and accountability" 84 + } 85 + } 86 + }
+32
mod.ts
··· 1 + /** 2 + * ATProtocol Autonomy Declaration Lexicon 3 + * 4 + * This package exports the lexicon definition for the autonomy declaration record type. 5 + * Used for declaring automation and AI usage for transparency and accountability on ATProtocol/Bluesky. 6 + * 7 + * @module 8 + */ 9 + 10 + import lexicon from "./lexicon/autonomy-declaration.json" with { type: "json" }; 11 + 12 + export const AUTONOMY_DECLARATION_LEXICON = lexicon; 13 + 14 + export type AutomationLevel = "human" | "assisted" | "collaborative" | "automated"; 15 + export type ResponsiblePartyType = "person" | "organization"; 16 + 17 + export interface ResponsibleParty { 18 + type?: ResponsiblePartyType; 19 + name?: string; 20 + contact?: string; 21 + did?: string; 22 + } 23 + 24 + export interface AutonomyDeclaration { 25 + automationLevel?: AutomationLevel; 26 + usesGenerativeAI?: boolean; 27 + description?: string; 28 + responsibleParty?: ResponsibleParty; 29 + disclosureUrl?: string; 30 + externalServices?: string[]; 31 + createdAt: string; 32 + }
+115
scripts/publish_lexicon.ts
··· 1 + #!/usr/bin/env -S deno run --allow-net --allow-read --allow-env 2 + 3 + /** 4 + * Schema Publication Script 5 + * 6 + * This script publishes the studio.voyager.account.autonomy lexicon schema 7 + * to the voyager.studio PDS. 8 + * 9 + * Prerequisites: 10 + * 1. DNS TXT record: _lexicon.account.voyager.studio pointing to your DID 11 + * 2. VOYAGER_PASSWORD: App password for voyager.studio account 12 + * 13 + * Usage: 14 + * Set VOYAGER_PASSWORD in .env and run: deno task publish:lexicon 15 + */ 16 + 17 + import { AtpAgent } from "npm:@atproto/api@^0.13.0"; 18 + import { AUTONOMY_DECLARATION_LEXICON } from "../mod.ts"; 19 + 20 + const VOYAGER_HANDLE = "voyager.studio"; 21 + const SERVICE_URL = "https://bsky.social"; 22 + 23 + const publishSchema = async () => { 24 + console.log("๐Ÿ”ง Publishing studio.voyager.account.autonomy schema...\n"); 25 + 26 + // Get password from environment 27 + const password = Deno.env.get("VOYAGER_PASSWORD"); 28 + 29 + if (!password) { 30 + console.error("โŒ Error: VOYAGER_PASSWORD not found.\n"); 31 + console.error(" Set VOYAGER_PASSWORD in .env\n"); 32 + Deno.exit(1); 33 + } 34 + 35 + // Initialize agent 36 + const agent = new AtpAgent({ service: SERVICE_URL }); 37 + 38 + try { 39 + // Login 40 + console.log(`๐Ÿ” Logging in as ${VOYAGER_HANDLE}...`); 41 + await agent.login({ 42 + identifier: VOYAGER_HANDLE, 43 + password: password, 44 + }); 45 + console.log(`โœ… Logged in as: ${agent.session?.handle} (${agent.session?.did})\n`); 46 + 47 + // Prepare schema record 48 + const schemaRecord = { 49 + $type: "com.atproto.lexicon.schema", 50 + lexicon: AUTONOMY_DECLARATION_LEXICON.lexicon, 51 + id: AUTONOMY_DECLARATION_LEXICON.id, 52 + defs: AUTONOMY_DECLARATION_LEXICON.defs, 53 + description: 54 + "Lexicon for declaring AI agent autonomy and automation practices for transparency and accountability", 55 + }; 56 + 57 + console.log(`๐Ÿ“‹ Schema Details:`); 58 + console.log(` NSID: ${schemaRecord.id}`); 59 + console.log(` Lexicon Version: ${schemaRecord.lexicon}`); 60 + console.log(` Definitions: ${Object.keys(schemaRecord.defs).join(", ")}\n`); 61 + 62 + // Check if schema already exists 63 + let existingSchema = null; 64 + try { 65 + const existing = await agent.com.atproto.repo.getRecord({ 66 + repo: agent.session?.did!, 67 + collection: "com.atproto.lexicon.schema", 68 + rkey: "studio.voyager.account.autonomy", 69 + }); 70 + existingSchema = existing.data; 71 + console.log("๐Ÿ“„ Existing schema found - will update\n"); 72 + } catch (error: any) { 73 + if (error?.status === 400 || error?.status === 404) { 74 + console.log("๐Ÿ“„ No existing schema found - will create new\n"); 75 + } else { 76 + throw error; 77 + } 78 + } 79 + 80 + // Publish schema 81 + console.log("๐Ÿ“ค Publishing schema..."); 82 + const result = await agent.com.atproto.repo.putRecord({ 83 + repo: agent.session?.did!, 84 + collection: "com.atproto.lexicon.schema", 85 + rkey: "studio.voyager.account.autonomy", 86 + record: schemaRecord, 87 + }); 88 + 89 + console.log(`โœ… Schema ${existingSchema ? "updated" : "published"} successfully!\n`); 90 + console.log(`๐Ÿ“ AT-URI: ${result.uri}`); 91 + console.log(`๐Ÿ”— CID: ${result.cid}\n`); 92 + 93 + // Verify DNS setup 94 + console.log("๐Ÿ” Next steps:"); 95 + console.log(" 1. Verify DNS TXT record is set:"); 96 + console.log(" Name: _lexicon.account.voyager.studio"); 97 + console.log(" Type: TXT"); 98 + console.log(` Value: did=${agent.session?.did}`); 99 + console.log( 100 + "\n 2. Test resolution (may take time for DNS to propagate):", 101 + ); 102 + console.log( 103 + ` dig TXT _lexicon.account.voyager.studio\n`, 104 + ); 105 + console.log("โœจ Schema is now discoverable by AT Protocol resolvers!"); 106 + } catch (error) { 107 + console.error("โŒ Error publishing schema:", error); 108 + Deno.exit(1); 109 + } 110 + }; 111 + 112 + // Run if executed directly 113 + if (import.meta.main) { 114 + await publishSchema(); 115 + }