Download docs/sovereign-emulator.html from Snapkitty/snapkitty-clojure-lisp-bridge: direct link, hf CLI and curl.
- Browser
- Download file 104 kB
-
https://huggingface.co/Snapkitty/snapkitty-clojure-lisp-bridge/resolve/main/docs/sovereign-emulator.html
- Command line
-
hf download hf://Snapkitty/snapkitty-clojure-lisp-bridge/docs/sovereign-emulator.html
-
curl -L -o sovereign-emulator.html https://huggingface.co/Snapkitty/snapkitty-clojure-lisp-bridge/resolve/main/docs/sovereign-emulator.html
104 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> | |
| <title>SnapKitty — Sovereign Agent Emulator</title> | |
| <meta name="apple-mobile-web-app-capable" content="yes" /> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> | |
| <meta name="apple-mobile-web-app-title" content="SnapKitty" /> | |
| <meta name="theme-color" content="#7c3aed" /> | |
| <link rel="manifest" href="manifest.json" /> | |
| <script> | |
| (function(){ | |
| var c=document.createElement('canvas');c.width=c.height=192; | |
| var x=c.getContext('2d'); | |
| x.fillStyle='#04040a';x.fillRect(0,0,192,192); | |
| x.fillStyle='#7c3aed';x.beginPath();x.arc(96,96,80,0,Math.PI*2);x.fill(); | |
| x.fillStyle='#fff';x.font='bold 72px monospace';x.textAlign='center';x.textBaseline='middle'; | |
| x.fillText('⬡',96,100); | |
| var png=c.toDataURL('image/png'); | |
| var l=document.createElement('link');l.rel='apple-touch-icon';l.href=png; | |
| document.head.appendChild(l); | |
| })(); | |
| </script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" /> | |
| <style> | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100%;overflow:hidden} | |
| body{font-family:"Space Mono",monospace;background:#0d0d0f;color:#e8e8f0;display:flex;flex-direction:column} | |
| ::-webkit-scrollbar{width:4px;height:4px} | |
| ::-webkit-scrollbar-track{background:#0a0a0e} | |
| ::-webkit-scrollbar-thumb{background:#1e1e2a;border-radius:2px} | |
| button{cursor:pointer;font-family:"Space Mono",monospace} | |
| input,select,textarea{font-family:"Space Mono",monospace} | |
| #topbar{height:36px;background:#0a0a0e;border-bottom:1px solid #1e1e2a;display:flex;align-items:center;padding:0 12px;gap:12px;flex-shrink:0;position:relative;z-index:100} | |
| #topbar .logo{color:#00ff88;font-weight:700;font-size:11px;letter-spacing:2px} | |
| #topbar .ver{color:#444;font-size:10px} | |
| #topbar .menus{display:flex;gap:4px;margin-left:8px;position:relative} | |
| .menu-item{position:relative} | |
| .menu-item > button.menu-btn{background:none;border:none;color:#888;font-size:10px;padding:4px 8px;border-radius:3px} | |
| .menu-item > button.menu-btn:hover,.menu-item.open > button.menu-btn{background:#1e1e2a;color:#e8e8f0} | |
| .menu-dropdown{display:none;position:absolute;top:100%;left:0;min-width:180px;background:#131318;border:1px solid #1e1e2a;border-radius:4px;padding:4px 0;box-shadow:0 8px 24px rgba(0,0,0,.6);z-index:200} | |
| .menu-item.open .menu-dropdown{display:block} | |
| .menu-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;color:#ccc;font-size:10px;padding:6px 12px} | |
| .menu-dropdown button:hover{background:#1e1e2a;color:#00ff88} | |
| .menu-dropdown .sep{height:1px;background:#1e1e2a;margin:4px 0} | |
| .menu-dropdown .hint{float:right;color:#444;font-size:9px} | |
| #topbar .status{display:flex;gap:12px;margin-left:auto;align-items:center} | |
| #topbar .status span{color:#444;font-size:10px} | |
| #sim-badge{font-size:9px;padding:2px 6px;border-radius:3px;border:1px solid #1e1e2a} | |
| #sim-badge.running{color:#00ff88;border-color:#00ff8833} | |
| #sim-badge.paused{color:#ffd60a;border-color:#ffd60a33} | |
| #main{flex:1;display:flex;overflow:hidden} | |
| #left{width:220px;background:#131318;border-right:1px solid #1e1e2a;display:flex;flex-direction:column;overflow:hidden;flex-shrink:0} | |
| #left .panel-title{padding:8px 10px;border-bottom:1px solid #1e1e2a;font-size:9px;color:#888;letter-spacing:1.5px;text-transform:uppercase} | |
| #tree{flex:1;overflow-y:auto} | |
| .group-header{padding:5px 8px;font-size:9px;color:#888;letter-spacing:1.5px;cursor:pointer;display:flex;align-items:center;gap:4px;border-bottom:1px solid #1e1e2a18;user-select:none} | |
| .group-header .count{margin-left:auto;color:#333;font-size:8px} | |
| .group-header .arrow{font-size:7px;transition:transform .15s} | |
| .group-header.open .arrow{transform:rotate(90deg)} | |
| .agent-row{padding:5px 10px 5px 22px;cursor:pointer;font-size:10px;display:flex;align-items:center;gap:7px;user-select:none;transition:background .15s;border-left:2px solid transparent} | |
| .agent-row:hover{background:#ffffff08} | |
| .agent-row.active{border-left-color:var(--agent-color);background:color-mix(in srgb,var(--agent-color) 10%,transparent)} | |
| .agent-row .name{color:#e8e8f0} | |
| .agent-row.active .name{color:var(--agent-color)} | |
| .agent-row .dot{margin-left:auto;width:5px;height:5px;border-radius:50%;background:var(--agent-color);flex-shrink:0;display:none} | |
| .agent-row.has-msgs .dot{display:block} | |
| #center{flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0f;position:relative;overflow:hidden} | |
| #canvas-grid{position:absolute;inset:0;background-image:repeating-linear-gradient(0deg,#1e1e2a22 0,#1e1e2a22 1px,transparent 1px,transparent 32px),repeating-linear-gradient(90deg,#1e1e2a22 0,#1e1e2a22 1px,transparent 1px,transparent 32px)} | |
| #phone-label{position:absolute;top:16px;left:50%;transform:translateX(-50%);font-size:9px;color:#333;letter-spacing:2px;white-space:nowrap} | |
| #phone{width:300px;height:630px;background:#1a1a1a;border-radius:44px;box-shadow:0 0 0 2px #333,0 0 0 4px #222,0 30px 80px rgba(0,0,0,.8);position:relative;flex-shrink:0;z-index:1;display:flex;flex-direction:column;overflow:hidden;border:1px solid #2a2a2a;transition:box-shadow .3s} | |
| .side-btn{position:absolute;width:3px;height:40px;background:#333;border-radius:2px 0 0 2px} | |
| .side-btn-right{right:-3px;top:140px;height:70px;border-radius:0 2px 2px 0} | |
| #screen{margin:10px 6px 6px;flex:1;background:#000;border-radius:36px;overflow:hidden;display:flex;flex-direction:column;position:relative} | |
| #punch{position:absolute;top:10px;left:50%;transform:translateX(-50%);width:10px;height:10px;background:#000;border-radius:50%;border:1px solid #222;z-index:10} | |
| #statusbar{height:28px;background:#050508;display:flex;align-items:center;padding:0 16px;flex-shrink:0;z-index:5} | |
| #clock{font-size:9px;color:#e8e8f0;font-weight:700} | |
| #status-icons{display:flex;align-items:center;gap:5px;margin-left:auto} | |
| #screen-content{flex:1;position:relative;overflow:hidden} | |
| #home-screen{height:100%;background:linear-gradient(160deg,#050510,#0a0a18);display:flex;flex-direction:column} | |
| #home-label{padding:8px 14px 4px;font-size:9px;color:#333} | |
| #app-grid{padding:4px 10px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px;flex:1;align-content:start;overflow-y:auto} | |
| .app-icon{display:flex;flex-direction:column;align-items:center;gap:3px;cursor:pointer;user-select:none} | |
| .app-icon .icon-wrap{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 2px 8px rgba(0,0,0,.3);transition:transform .1s} | |
| .app-icon:active .icon-wrap{transform:scale(.9)} | |
| .app-icon span{font-size:7.5px;color:#888;text-align:center;line-height:1.2} | |
| #dock{height:72px;border-top:1px solid #1e1e2a;display:flex;align-items:center;justify-content:space-around;padding:0 12px;flex-shrink:0} | |
| .dock-icon{width:42px;height:42px;border-radius:12px;background:#1e1e2a;display:flex;align-items:center;justify-content:center;font-size:16px;cursor:pointer;border:1px solid transparent;transition:border .15s,background .15s} | |
| .dock-icon:hover{border-color:#00ff8844;background:#1e1e2aee} | |
| #navbar-home,#navbar-chat,#navbar-app{height:28px;display:flex;align-items:center;justify-content:space-around;padding:0 40px;flex-shrink:0} | |
| #navbar-home span,#navbar-chat span,#navbar-app span{color:#444;font-size:14px;cursor:pointer} | |
| #navbar-home span:hover,#navbar-chat span:hover,#navbar-app span:hover{color:#888} | |
| #chat-screen,#browser-screen,#term-screen,#worm-screen,#const-screen{height:100%;display:none;flex-direction:column;background:#030306} | |
| #chat-appbar,.app-bar{padding:8px 12px;display:flex;align-items:center;gap:8px;border-bottom:1px solid #1e1e2a;background:#050508;flex-shrink:0} | |
| .back-btn{background:none;border:none;font-size:14px;padding:0;color:#00ff88} | |
| #chat-agent-icon{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0} | |
| #chat-agent-name{font-size:11px;font-weight:700} | |
| #chat-agent-role{font-size:8px;color:#888} | |
| #chat-status-dot{margin-left:auto;width:7px;height:7px;border-radius:50%;background:#00ff88;box-shadow:0 0 6px #00ff88;flex-shrink:0} | |
| #messages{flex:1;overflow-y:auto;padding:10px 10px 6px;display:flex;flex-direction:column;gap:6px} | |
| .msg{display:flex} | |
| .msg.user{justify-content:flex-end} | |
| .msg.agent{justify-content:flex-start} | |
| .bubble{max-width:78%;padding:6px 9px;font-size:9.5px;line-height:1.5;word-break:break-word;white-space:pre-wrap} | |
| .bubble.user{background:#0a84ff;border-radius:12px 12px 2px 12px;color:#fff} | |
| .bubble.agent{border-radius:12px 12px 12px 2px;color:#e8e8f0} | |
| .bubble.sys{background:#1e1e2a;border-radius:8px;color:#888;font-size:8px;max-width:100%} | |
| #empty-state{text-align:center;margin-top:40px;color:#444;font-size:9px} | |
| #empty-icon{font-size:28px;margin-bottom:8px} | |
| #input-bar{padding:6px 8px;border-top:1px solid #1e1e2a;display:flex;gap:6px;background:#050508;flex-shrink:0} | |
| #msg-input{flex:1;background:#0a0a14;border:1px solid #1e1e2a;border-radius:20px;padding:6px 10px;color:#e8e8f0;font-size:9.5px;outline:none} | |
| #send-btn{width:30px;height:30px;border-radius:50%;border:none;font-size:13px;flex-shrink:0;transition:opacity .15s} | |
| #send-btn:disabled{opacity:.4} | |
| /* Browser */ | |
| #browser-toolbar{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #1e1e2a;background:#050508;flex-shrink:0;align-items:center} | |
| #browser-toolbar button{background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;border-radius:6px;width:26px;height:26px;font-size:11px} | |
| #browser-url{flex:1;background:#0a0a14;border:1px solid #1e1e2a;border-radius:12px;padding:4px 8px;color:#e8e8f0;font-size:9px;outline:none} | |
| #browser-frame-wrap{flex:1;position:relative;background:#0a0a14;overflow:hidden} | |
| #browser-frame{width:100%;height:100%;border:none;background:#fff} | |
| #browser-fallback{position:absolute;inset:0;display:none;flex-direction:column;padding:10px;overflow:auto;background:#030306} | |
| #browser-fallback .url-line{font-size:8px;color:#0a84ff;margin-bottom:8px;word-break:break-all} | |
| #browser-fallback pre{font-size:8px;color:#aaa;white-space:pre-wrap;word-break:break-word;line-height:1.4} | |
| #browser-status{font-size:8px;color:#444;padding:4px 8px;border-top:1px solid #1e1e2a;flex-shrink:0} | |
| /* Terminal */ | |
| #term-out{flex:1;overflow-y:auto;padding:8px;font-size:9px;line-height:1.55;color:#00ff88;background:#020208} | |
| #term-out .in{color:#0a84ff} | |
| #term-out .out{color:#c8c8d0} | |
| #term-out .err{color:#ff453a} | |
| #term-out .sys{color:#888} | |
| #term-in-bar{display:flex;gap:4px;padding:6px 8px;border-top:1px solid #1e1e2a;background:#050508;flex-shrink:0;align-items:center} | |
| #term-prompt{font-size:9px;color:#00ff88;flex-shrink:0} | |
| #term-input{flex:1;background:transparent;border:none;color:#e8e8f0;font-size:9.5px;outline:none} | |
| #term-agent-sel{background:#0a0a14;border:1px solid #1e1e2a;color:#ccc;font-size:8px;border-radius:4px;padding:2px 4px;max-width:90px} | |
| /* WORM / Constitution shared */ | |
| .scroll-panel{flex:1;overflow-y:auto;padding:8px;font-size:9px;line-height:1.5} | |
| .worm-entry{border:1px solid #1e1e2a;border-radius:6px;padding:6px 8px;margin-bottom:6px;background:#0a0a14} | |
| .worm-entry .meta{color:#444;font-size:8px;margin-bottom:4px} | |
| .worm-entry .hash{color:#0a84ff;font-size:7.5px;word-break:break-all} | |
| .worm-entry .body{color:#ccc} | |
| .const-rule{border-left:2px solid #bf5af2;padding:4px 8px;margin-bottom:6px;background:#0a0a14} | |
| .const-rule .pred{color:#bf5af2} | |
| .const-rule .body{color:#888;font-size:8px} | |
| .badge{display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px} | |
| .badge.ok{background:#00ff8822;color:#00ff88} | |
| .badge.deny{background:#ff453a22;color:#ff453a} | |
| .badge.pend{background:#ffd60a22;color:#ffd60a} | |
| #incoming-call{position:absolute;inset:0;background:rgba(0,0,0,.95);z-index:20;display:none;flex-direction:column;align-items:center;justify-content:center;gap:16px} | |
| #call-icon{font-size:40px} | |
| #call-name{font-size:14px;font-weight:700} | |
| #call-role{font-size:9px;color:#888} | |
| #call-label{font-size:9px;color:#333} | |
| #call-btns{display:flex;gap:24px;margin-top:24px} | |
| #call-btns button{width:52px;height:52px;border-radius:50%;border:none;font-size:20px} | |
| #right{width:240px;background:#131318;border-left:1px solid #1e1e2a;display:flex;flex-direction:column;overflow:hidden;flex-shrink:0} | |
| #right .panel-title{padding:8px 10px;border-bottom:1px solid #1e1e2a;font-size:9px;color:#888;letter-spacing:1.5px;text-transform:uppercase} | |
| #inspector-card{padding:12px 10px;border-bottom:1px solid #1e1e2a} | |
| #inspector-agent-row{display:flex;align-items:center;gap:8px;margin-bottom:8px} | |
| #inspector-icon{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0} | |
| #inspector-name{font-size:11px;font-weight:700} | |
| #inspector-role{font-size:8.5px;color:#888} | |
| #inspector-group{font-size:7.5px;color:#333} | |
| .prop-row{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid #1e1e2a20} | |
| .prop-key{font-size:8.5px;color:#444} | |
| .prop-val{font-size:8.5px;color:#e8e8f0} | |
| .prop-val.online{color:#00ff88} | |
| #gate-scores{padding:10px;border-bottom:1px solid #1e1e2a} | |
| #gate-scores .section-label,.section-label{font-size:9px;color:#888;letter-spacing:1px;margin-bottom:8px} | |
| .gate-row{margin-bottom:6px} | |
| .gate-header{display:flex;justify-content:space-between;margin-bottom:2px} | |
| .gate-label{font-size:8px;color:#444} | |
| .gate-val{font-size:8px} | |
| .gate-bar-bg{height:3px;background:#1e1e2a;border-radius:2px} | |
| .gate-bar-fill{height:100%;border-radius:2px;transition:width .5s} | |
| #agent-grid-panel{flex:1;overflow-y:auto;padding:8px} | |
| #agent-grid-icons{display:flex;flex-wrap:wrap;gap:5px} | |
| .grid-icon{width:28px;height:28px;border-radius:7px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:13px;transition:border .15s} | |
| #bottom{height:130px;border-top:1px solid #1e1e2a;background:#080810;flex-shrink:0;display:flex;flex-direction:column} | |
| #logcat-header{padding:4px 10px;border-bottom:1px solid #1e1e2a;display:flex;align-items:center;gap:12px;flex-shrink:0} | |
| #logcat-header .label{font-size:9px;color:#888;letter-spacing:1.5px} | |
| #logcat-header .subject{font-size:8px;color:#333} | |
| #nats-status{font-size:8px} | |
| #logcat-body{flex:1;overflow-y:auto;padding:4px 10px} | |
| .log-entry{font-size:8.5px;line-height:1.7} | |
| .log-ts{color:#333} | |
| .log-subject{color:#0a84ff} | |
| .log-subject.error{color:#ff453a} | |
| .log-subject.warn{color:#ffd60a} | |
| .log-payload{color:#888} | |
| #help-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:300;align-items:center;justify-content:center} | |
| #help-modal.open{display:flex} | |
| #help-box{background:#131318;border:1px solid #1e1e2a;border-radius:10px;padding:16px 20px;max-width:420px;width:90%;max-height:80vh;overflow:auto} | |
| #help-box h2{font-size:12px;color:#00ff88;margin-bottom:10px} | |
| #help-box table{width:100%;border-collapse:collapse;font-size:10px} | |
| #help-box td{padding:4px 6px;border-bottom:1px solid #1e1e2a} | |
| #help-box td:first-child{color:#0a84ff;width:60px} | |
| #help-box .close-help{margin-top:12px;background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;padding:6px 12px;border-radius:4px;font-size:10px} | |
| @media (max-width:600px),(display-mode:standalone){ | |
| #left,#right,#canvas-grid,#phone-label{display:none} | |
| #center{background:#000;align-items:stretch;justify-content:stretch} | |
| #phone{width:100%;height:100%;border-radius:0;box-shadow:none;border:none;background:#000} | |
| .side-btn{display:none} | |
| #screen{margin:0;border-radius:0;background:#050508} | |
| #statusbar{padding-top:env(safe-area-inset-top);height:calc(28px + env(safe-area-inset-top))} | |
| #punch{display:none} | |
| #main{flex-direction:column} | |
| #bottom{height:120px;flex-shrink:0} | |
| .app-icon .icon-wrap{width:54px;height:54px;font-size:22px} | |
| .dock-icon{width:52px;height:52px;font-size:20px} | |
| #msg-input{font-size:12px;padding:8px 12px} | |
| .bubble{font-size:11px} | |
| #chat-agent-name{font-size:13px} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="topbar"> | |
| <span class="logo">SOVEREIGN IDE</span> | |
| <span class="ver">v∞.1</span> | |
| <div class="menus"> | |
| <div class="menu-item" data-menu="file"> | |
| <button class="menu-btn" type="button">File</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="worm-dump">Dump World State <span class="hint">W</span></button> | |
| <button type="button" data-action="export-json">Export Session JSON</button> | |
| <div class="sep"></div> | |
| <button type="button" data-action="clear-ledger">Clear Ledger (testnet) <span class="hint">C</span></button> | |
| </div> | |
| </div> | |
| <div class="menu-item" data-menu="edit"> | |
| <button class="menu-btn" type="button">Edit</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="focus-term">Focus Terminal</button> | |
| <button type="button" data-action="focus-browser">Focus Browser</button> | |
| </div> | |
| </div> | |
| <div class="menu-item" data-menu="view"> | |
| <button class="menu-btn" type="button">View</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="open-home">Home</button> | |
| <button type="button" data-action="open-worm">WORM Ledger</button> | |
| <button type="button" data-action="open-const">Constitution <span class="hint">?</span></button> | |
| <button type="button" data-action="open-term">Terminal</button> | |
| <button type="button" data-action="open-browser">Browser</button> | |
| <button type="button" data-action="open-vm">VM Editor</button> | |
| </div> | |
| </div> | |
| <div class="menu-item" data-menu="run"> | |
| <button class="menu-btn" type="button">Run</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="step">Step Clock <span class="hint">Space</span></button> | |
| <button type="button" data-action="toggle-pause">Pause / Resume <span class="hint">P</span></button> | |
| <button type="button" data-action="verify">Verify Chain <span class="hint">V</span></button> | |
| </div> | |
| </div> | |
| <div class="menu-item" data-menu="tools"> | |
| <button class="menu-btn" type="button">Tools</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="open-term">Bash In / Out</button> | |
| <button type="button" data-action="open-browser">Web Browser</button> | |
| <button type="button" data-action="incoming-call">Simulate Incoming Call</button> | |
| <button type="button" data-action="agent-tick">Force Agent Tick</button> | |
| </div> | |
| </div> | |
| <div class="menu-item" data-menu="help"> | |
| <button class="menu-btn" type="button">Help</button> | |
| <div class="menu-dropdown"> | |
| <button type="button" data-action="show-help">Keyboard Shortcuts</button> | |
| <button type="button" data-action="open-const">Show Constitution</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status"> | |
| <span id="sim-badge" class="running">SIM ● RUN</span> | |
| <span>WORM: <span id="worm-count" style="color:#0a84ff">0</span></span> | |
| <span>Mesh: <span style="color:#00ff88">ONLINE</span></span> | |
| <span>NATS: <span id="nats-dot">●</span></span> | |
| </div> | |
| </div> | |
| <div id="main"> | |
| <div id="left"> | |
| <div class="panel-title">Project / Agents</div> | |
| <div id="tree"></div> | |
| </div> | |
| <div id="center"> | |
| <div id="canvas-grid"></div> | |
| <div id="phone-label">Google Pixel 9 Pro • SnapKitty OS • 6.1"</div> | |
| <div id="phone"> | |
| <div class="side-btn" style="left:-3px;top:120px"></div> | |
| <div class="side-btn" style="left:-3px;top:175px"></div> | |
| <div class="side-btn side-btn-right"></div> | |
| <div id="screen"> | |
| <div id="punch"></div> | |
| <div id="statusbar"> | |
| <span id="clock">00:00</span> | |
| <div id="status-icons"> | |
| <canvas id="signal-canvas" width="22" height="14"></canvas> | |
| <span style="font-size:9px;color:#e8e8f0">5G</span> | |
| <canvas id="battery-canvas" width="22" height="10"></canvas> | |
| </div> | |
| </div> | |
| <div id="screen-content"> | |
| <div id="incoming-call"> | |
| <div id="call-icon"></div> | |
| <div id="call-name"></div> | |
| <div id="call-role"></div> | |
| <div id="call-label">Incoming sovereign call…</div> | |
| <div id="call-btns"> | |
| <button id="decline-btn" style="background:#ff453a">✕</button> | |
| <button id="answer-btn" style="background:#00ff88">✓</button> | |
| </div> | |
| </div> | |
| <div id="home-screen" style="display:flex"> | |
| <div id="home-label">SnapKitty OS • Agents · Browser · Bash</div> | |
| <div id="app-grid"></div> | |
| <div id="dock"> | |
| <div class="dock-icon" data-dock="phone" title="Call">📞</div> | |
| <div class="dock-icon" data-dock="messages" title="Messages">💬</div> | |
| <div class="dock-icon" data-dock="browser" title="Browser">🌐</div> | |
| <div class="dock-icon" data-dock="term" title="Terminal">⌨</div> | |
| </div> | |
| <div id="navbar-home"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| <div id="chat-screen"> | |
| <div id="chat-appbar"> | |
| <button id="chat-back" class="back-btn" type="button">◁</button> | |
| <div id="chat-agent-icon"></div> | |
| <div> | |
| <div id="chat-agent-name"></div> | |
| <div id="chat-agent-role"></div> | |
| </div> | |
| <div id="chat-status-dot"></div> | |
| </div> | |
| <div id="messages"> | |
| <div id="empty-state"> | |
| <div id="empty-icon"></div> | |
| <div id="empty-text"></div> | |
| <div id="empty-sub"></div> | |
| </div> | |
| </div> | |
| <div id="input-bar"> | |
| <input id="msg-input" placeholder="Message agent… /browse /bash" /> | |
| <button id="send-btn" type="button" disabled>▶</button> | |
| </div> | |
| <div id="navbar-chat"> | |
| <span id="nav-back">◁</span> | |
| <span id="nav-home-btn">●</span> | |
| <span id="nav-recents-chat">□</span> | |
| </div> | |
| </div> | |
| <div id="browser-screen"> | |
| <div class="app-bar"> | |
| <button class="back-btn" type="button" data-go-home>◁</button> | |
| <div style="flex:1"> | |
| <div style="font-size:11px;font-weight:700;color:#0a84ff">BROWSER</div> | |
| <div style="font-size:8px;color:#888">Agent web · governed fetch</div> | |
| </div> | |
| </div> | |
| <div id="browser-toolbar"> | |
| <button type="button" id="browser-back" title="Back">←</button> | |
| <button type="button" id="browser-fwd" title="Forward">→</button> | |
| <button type="button" id="browser-reload" title="Reload">↻</button> | |
| <input id="browser-url" placeholder="https://… or search" /> | |
| <button type="button" id="browser-go" title="Go">▶</button> | |
| </div> | |
| <div id="browser-frame-wrap"> | |
| <iframe id="browser-frame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" title="Sovereign Browser"></iframe> | |
| <div id="browser-fallback"> | |
| <div class="url-line" id="fallback-url"></div> | |
| <pre id="fallback-body"></pre> | |
| </div> | |
| </div> | |
| <div id="browser-status">idle</div> | |
| <div id="navbar-app"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| <div id="term-screen"> | |
| <div class="app-bar"> | |
| <button class="back-btn" type="button" data-go-home>◁</button> | |
| <div style="flex:1"> | |
| <div style="font-size:11px;font-weight:700;color:#00ff88">BASH · IN / OUT</div> | |
| <div style="font-size:8px;color:#888">Virtual shell · constitution-gated</div> | |
| </div> | |
| <select id="term-agent-sel" title="Acting agent"></select> | |
| </div> | |
| <div id="term-out"></div> | |
| <div id="term-in-bar"> | |
| <span id="term-prompt">$</span> | |
| <input id="term-input" autocomplete="off" spellcheck="false" placeholder="bash in — type help" /> | |
| </div> | |
| <div id="navbar-app"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| <div id="worm-screen"> | |
| <div class="app-bar"> | |
| <button class="back-btn" type="button" data-go-home>◁</button> | |
| <div style="flex:1"> | |
| <div style="font-size:11px;font-weight:700;color:#0a84ff">WORM LEDGER</div> | |
| <div style="font-size:8px;color:#888">Append-only · SHA-256 chained</div> | |
| </div> | |
| <button type="button" id="worm-verify-btn" style="background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;font-size:8px;padding:4px 8px;border-radius:4px">Verify</button> | |
| </div> | |
| <div class="scroll-panel" id="worm-list"></div> | |
| <div id="navbar-app"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| <div id="const-screen"> | |
| <div class="app-bar"> | |
| <button class="back-btn" type="button" data-go-home>◁</button> | |
| <div style="flex:1"> | |
| <div style="font-size:11px;font-weight:700;color:#bf5af2">CONSTITUTION</div> | |
| <div style="font-size:8px;color:#888">Prolog-style can_execute/2</div> | |
| </div> | |
| </div> | |
| <div class="scroll-panel" id="const-list"></div> | |
| <div id="navbar-app"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VM Editor Screen --> | |
| <div id="vm-editor-screen" style="display:none;flex-direction:column;background:#030306"> | |
| <div class="app-bar"> | |
| <button class="back-btn" type="button" data-go-home>◁</button> | |
| <div style="flex:1"> | |
| <div style="font-size:11px;font-weight:700;color:#00ff88">VM EDITOR</div> | |
| <div style="font-size:8px;color:#888">Virtual Machine State Editor</div> | |
| </div> | |
| <button type="button" id="vm-save-btn" style="background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;font-size:8px;padding:4px 8px;border-radius:4px">Save</button> | |
| </div> | |
| <div class="scroll-panel" id="vm-editor-content"> | |
| <div style="padding:8px"> | |
| <div class="section-label">Virtual Filesystem</div> | |
| <div id="vfs-editor" style="margin-bottom:12px"> | |
| <div style="display:flex;gap:4px;margin-bottom:6px"> | |
| <input id="vfs-path-input" placeholder="/path/to/file" style="flex:1;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px 8px;color:#e8e8f0;font-size:9px;outline:none" /> | |
| <button type="button" id="vfs-read-btn" style="background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:4px 8px;font-size:8px">Read</button> | |
| <button type="button" id="vfs-write-btn" style="background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:4px 8px;font-size:8px">Write</button> | |
| <button type="button" id="vfs-new-btn" style="background:#1e1e2a;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:4px 8px;font-size:8px">New</button> | |
| </div> | |
| <textarea id="vfs-file-editor" style="width:100%;height:120px;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:8px;color:#e8e8f0;font-size:9px;outline:none;font-family:monospace;resize:none"></textarea> | |
| </div> | |
| <div class="section-label">VM State</div> | |
| <div id="vm-state-editor" style="display:grid;grid-template-columns:1fr 1fr;gap:8px"> | |
| <div> | |
| <div style="font-size:8px;color:#444;margin-bottom:4px">Tick</div> | |
| <input id="vm-tick-input" type="number" style="width:100%;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px;color:#e8e8f0;font-size:9px;outline:none" /> | |
| </div> | |
| <div> | |
| <div style="font-size:8px;color:#444;margin-bottom:4px">Paused</div> | |
| <select id="vm-paused-select" style="width:100%;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px;color:#e8e8f0;font-size:9px;outline:none"> | |
| <option value="false">Running</option> | |
| <option value="true">Paused</option> | |
| </select> | |
| </div> | |
| <div> | |
| <div style="font-size:8px;color:#444;margin-bottom:4px">Active Agent</div> | |
| <select id="vm-agent-select" style="width:100%;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px;color:#e8e8f0;font-size:9px;outline:none"></select> | |
| </div> | |
| <div> | |
| <div style="font-size:8px;color:#444;margin-bottom:4px">WORM Blocks</div> | |
| <input id="vm-worm-input" type="number" style="width:100%;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px;color:#e8e8f0;font-size:9px;outline:none" /> | |
| </div> | |
| </div> | |
| <div class="section-label">Council Configuration</div> | |
| <div id="council-config" style="display:flex;flex-direction:column;gap:6px"> | |
| <div style="display:flex;gap:4px"> | |
| <select id="council-available" style="flex:1;background:#0a0a14;border:1px solid #1e1e2a;border-radius:4px;padding:4px;color:#e8e8f0;font-size:9px;outline:none"></select> | |
| <button type="button" id="council-add-btn" style="background:#00ff88;border:1px solid #00ff88;color:#000;border-radius:4px;padding:4px 8px;font-size:8px">Add</button> | |
| </div> | |
| <div id="council-list" style="display:flex;flex-wrap:wrap;gap:4px"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="navbar-app"> | |
| <span data-nav="back">◁</span> | |
| <span data-nav="home">●</span> | |
| <span data-nav="recents">□</span> | |
| </div> | |
| </div> | |
| <!-- Council Vote Modal --> | |
| <div id="council-vote-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:400;align-items:center;justify-content:center"> | |
| <div style="background:#131318;border:1px solid #1e1e2a;border-radius:12px;padding:20px;max-width:400px;width:90%;max-height:80vh;overflow:auto"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"> | |
| <div style="font-size:12px;font-weight:700;color:#00ff88">COUNCIL VOTE</div> | |
| <button type="button" id="vote-close-btn" style="background:none;border:none;color:#888;font-size:14px;cursor:pointer">✕</button> | |
| </div> | |
| <div id="vote-proposal" style="background:#0a0a14;border-radius:6px;padding:12px;margin-bottom:12px"> | |
| <div style="font-size:9px;color:#444;margin-bottom:4px">Proposal</div> | |
| <div id="vote-agent" style="font-size:10px;color:#e8e8f0;margin-bottom:4px"></div> | |
| <div id="vote-action" style="font-size:10px;color:#0a84ff;margin-bottom:4px"></div> | |
| <div id="vote-payload" style="font-size:8px;color:#888;word-break:break-word"></div> | |
| </div> | |
| <div id="vote-results" style="margin-bottom:12px"> | |
| <div style="font-size:9px;color:#444;margin-bottom:6px">Votes Required: <span id="vote-required">3</span>/<span id="vote-total">6</span></div> | |
| <div id="vote-tally" style="display:flex;flex-direction:column;gap:4px"></div> | |
| </div> | |
| <div style="display:flex;gap:8px;justify-content:flex-end"> | |
| <button type="button" id="vote-approve-btn" style="background:#00ff88;border:1px solid #00ff88;color:#000;padding:8px 16px;border-radius:6px;font-size:10px;cursor:pointer">Approve</button> | |
| <button type="button" id="vote-deny-btn" style="background:#ff453a;border:1px solid #ff453a;color:#fff;padding:8px 16px;border-radius:6px;font-size:10px;cursor:pointer">Deny</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="right"> | |
| <div class="panel-title">Module Inspector</div> | |
| <div id="inspector-card"> | |
| <div id="inspector-agent-row"> | |
| <div id="inspector-icon"></div> | |
| <div> | |
| <div id="inspector-name"></div> | |
| <div id="inspector-role"></div> | |
| <div id="inspector-group"></div> | |
| </div> | |
| </div> | |
| <div id="props"></div> | |
| </div> | |
| <div id="gate-scores"> | |
| <div class="section-label">Gate Scores</div> | |
| <div id="gates"></div> | |
| </div> | |
| <div id="agent-grid-panel"> | |
| <div class="section-label" id="agent-count"></div> | |
| <div id="agent-grid-icons"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="bottom"> | |
| <div id="logcat-header"> | |
| <span class="label">LOGCAT</span> | |
| <span class="subject">snapkitty.></span> | |
| <span id="nats-status" style="color:#333">○ local runtime…</span> | |
| </div> | |
| <div id="logcat-body"></div> | |
| </div> | |
| <div id="help-modal"> | |
| <div id="help-box"> | |
| <h2>Keyboard Shortcuts</h2> | |
| <table> | |
| <tr><td>Space</td><td>Step agent clock</td></tr> | |
| <tr><td>P</td><td>Pause / resume simulation</td></tr> | |
| <tr><td>C</td><td>Clear ledger (testnet)</td></tr> | |
| <tr><td>V</td><td>Verify chain integrity</td></tr> | |
| <tr><td>W</td><td>Dump world state to WORM</td></tr> | |
| <tr><td>?</td><td>Show constitution</td></tr> | |
| <tr><td>B</td><td>Open browser</td></tr> | |
| <tr><td>T</td><td>Open terminal (bash)</td></tr> | |
| <tr><td>H</td><td>Home screen</td></tr> | |
| </table> | |
| <p style="margin-top:10px;font-size:9px;color:#666">Chat slash commands: <code>/browse url</code> · <code>/bash cmd</code> · <code>/worm</code> · <code>/help</code></p> | |
| <button type="button" class="close-help" id="close-help">Close</button> | |
| </div> | |
| </div> | |
| <script> | |
| // ── Agent manifest ───────────────────────────────────────────────────────────── | |
| const AGENTS = [ | |
| {key:'ahmad', label:'AHMAD', role:'Root Sovereign', color:'#ffd60a', icon:'♾', group:'SOVEREIGN', trust:'root', domain:'any'}, | |
| {key:'edaulc', label:'EDAULC', role:'Mirror', color:'#bf5af2', icon:'◈', group:'SOVEREIGN', trust:'high', domain:'any'}, | |
| {key:'bob', label:'BOB', role:'Orchestrator', color:'#00ff88', icon:'⬡', group:'SOVEREIGN', trust:'high', domain:'ops'}, | |
| {key:'sentinel', label:'SENTINEL', role:'Threat Watch', color:'#ff453a', icon:'🛡', group:'SECURITY', trust:'high', domain:'security'}, | |
| {key:'cipher', label:'CIPHER', role:'Cryptographic Core', color:'#0a84ff', icon:'🔐', group:'SECURITY', trust:'high', domain:'crypto'}, | |
| {key:'veil', label:'VEIL', role:'OPSEC Layer', color:'#ff453a', icon:'👁', group:'SECURITY', trust:'medium', domain:'security'}, | |
| {key:'ward', label:'WARD', role:'Perimeter Guard', color:'#ff453a', icon:'⚔', group:'SECURITY', trust:'medium', domain:'security'}, | |
| {key:'vault', label:'VAULT', role:'Treasury', color:'#ffd60a', icon:'🏦', group:'FINANCE', trust:'high', domain:'finance'}, | |
| {key:'ledger', label:'LEDGER', role:'Accounting', color:'#ffd60a', icon:'📒', group:'FINANCE', trust:'medium', domain:'finance'}, | |
| {key:'forge', label:'FORGE', role:'Package Mint', color:'#ffd60a', icon:'⚒', group:'FINANCE', trust:'medium', domain:'finance'}, | |
| {key:'atlas', label:'ATLAS', role:'World Model', color:'#0a84ff', icon:'🌐', group:'INTEL', trust:'high', domain:'intel'}, | |
| {key:'oracle', label:'ORACLE', role:'Prediction', color:'#bf5af2', icon:'🔮', group:'INTEL', trust:'medium', domain:'intel'}, | |
| {key:'prism', label:'PRISM', role:'Signal Refractor', color:'#0a84ff', icon:'◇', group:'INTEL', trust:'medium', domain:'intel'}, | |
| {key:'lens', label:'LENS', role:'Vision Core', color:'#0a84ff', icon:'🔭', group:'INTEL', trust:'medium', domain:'intel'}, | |
| {key:'herald', label:'HERALD', role:'Broadcast', color:'#00ff88', icon:'📡', group:'COMMS', trust:'medium', domain:'comms'}, | |
| {key:'nova', label:'NOVA', role:'Creative Voice', color:'#00ff88', icon:'✦', group:'COMMS', trust:'medium', domain:'comms'}, | |
| {key:'echo', label:'ECHO', role:'Resonance Check', color:'#00ff88', icon:'∿', group:'COMMS', trust:'low', domain:'comms'}, | |
| {key:'lyra', label:'LYRA', role:'Harmonic Analysis', color:'#bf5af2', icon:'♪', group:'COMMS', trust:'low', domain:'comms'}, | |
| {key:'mira', label:'MIRA', role:'Interface Mirror', color:'#00ff88', icon:'⌘', group:'COMMS', trust:'medium', domain:'comms'}, | |
| {key:'flux', label:'FLUX', role:'Chaos Injector', color:'#ff453a', icon:'⚡', group:'CHAOS', trust:'low', domain:'chaos'}, | |
| {key:'storm', label:'STORM', role:'Pressure Test', color:'#ff453a', icon:'🌪', group:'CHAOS', trust:'low', domain:'chaos'}, | |
| {key:'phantom', label:'PHANTOM', role:'Shadow Runner', color:'#ff453a', icon:'◉', group:'CHAOS', trust:'low', domain:'chaos'}, | |
| {key:'shade', label:'SHADE', role:'Dark Pattern', color:'#ff453a', icon:'⬟', group:'CHAOS', trust:'low', domain:'chaos'}, | |
| {key:'nexus', label:'NEXUS', role:'Mesh Hub', color:'#bf5af2', icon:'⬡', group:'BRIDGE', trust:'high', domain:'ops'}, | |
| {key:'bridge', label:'BRIDGE', role:'Protocol Bridge', color:'#bf5af2', icon:'⇌', group:'BRIDGE', trust:'medium', domain:'ops'}, | |
| {key:'dawn', label:'DAWN', role:'Cold Boot', color:'#ffd60a', icon:'☀', group:'BRIDGE', trust:'medium', domain:'ops'}, | |
| {key:'ember', label:'EMBER', role:'Warm Restart', color:'#ffd60a', icon:'🔥', group:'BRIDGE', trust:'medium', domain:'ops'}, | |
| {key:'axiom', label:'AXIOM', role:'Logic Core', color:'#0a84ff', icon:'∴', group:'CORE', trust:'high', domain:'core'}, | |
| {key:'mnemex', label:'MNEMEX', role:'Memory Engine', color:'#bf5af2', icon:'⊛', group:'CORE', trust:'high', domain:'core'}, | |
| {key:'stalas', label:'STALAS', role:'Stability Gate', color:'#00ff88', icon:'⊕', group:'CORE', trust:'high', domain:'core'}, | |
| {key:'loc', label:'LOC', role:'Line of Code', color:'#444', icon:'{}', group:'CORE', trust:'medium', domain:'core'}, | |
| {key:'metatron', label:'METATRON', role:'First Persona', color:'#bf5af2', icon:'✦', group:'LEGACY', trust:'medium', domain:'any'}, | |
| {key:'enki', label:'ENKI', role:'Sumerian Core', color:'#ffd60a', icon:'𒀭', group:'LEGACY', trust:'medium', domain:'any'}, | |
| {key:'influence',label:'INFLUENCE',role:'Network Reach', color:'#0a84ff', icon:'∞', group:'LEGACY', trust:'low', domain:'comms'}, | |
| ] | |
| const GROUPS = ['SOVEREIGN','SECURITY','FINANCE','INTEL','COMMS','CHAOS','BRIDGE','CORE','LEGACY'] | |
| const HOME_KEYS = ['ahmad','sentinel','vault','atlas','herald','cipher','oracle','bob','nova','forge','nexus','axiom'] | |
| const SYSTEM_APPS = [ | |
| {key:'_browser', label:'BROWSER', icon:'🌐', color:'#0a84ff', action:'browser'}, | |
| {key:'_term', label:'BASH', icon:'⌨', color:'#00ff88', action:'term'}, | |
| {key:'_worm', label:'WORM', icon:'⛓', color:'#0a84ff', action:'worm'}, | |
| {key:'_const', label:'CONST', icon:'📜', color:'#bf5af2', action:'const'}, | |
| {key:'_vm', label:'VM EDIT', icon:'⬡', color:'#ffd60a', action:'vm'}, | |
| ] | |
| const API = 'https://pbtju19b3e.execute-api.us-east-1.amazonaws.com/' | |
| const MESH = 'https://sovereign-mesh.snapkittywest.workers.dev' | |
| const CORS_PROXY = 'https://api.allorigins.win/raw?url=' | |
| const TRUST_RANK = { root:4, high:3, medium:2, low:1 } | |
| // ── State ────────────────────────────────────────────────────────────────────── | |
| let activeAgent = AGENTS[0] | |
| let screen = 'home' | |
| let messages = {} | |
| let expanded = {SOVEREIGN:true} | |
| let loading = false | |
| let simPaused = false | |
| let simTick = 0 | |
| let wormChain = [] | |
| let browserHistory = [] | |
| let browserIndex = -1 | |
| let termHistory = [] | |
| let termHistIdx = -1 | |
| let feedSince = 0 | |
| let natsCount = 0 | |
| let lastScreen = 'home' | |
| // Council Configuration - configurable council members for voting | |
| let councilMembers = ['ahmad', 'sentinel', 'cipher', 'vault', 'atlas', 'nexus'] | |
| let pendingVotes = {} | |
| let voteTimers = {} | |
| const VFS = { | |
| '/': ['agents/', 'worm/', 'constitution/', 'etc/', 'tmp/'], | |
| '/agents/': null, | |
| '/worm/': ['head.txt', 'count.txt'], | |
| '/constitution/': ['rules.pl', 'can_execute.pl'], | |
| '/etc/': ['hostname', 'version'], | |
| '/tmp/': [], | |
| } | |
| // ── Crypto / WORM ────────────────────────────────────────────────────────────── | |
| async function sha256(text) { | |
| const data = new TextEncoder().encode(text) | |
| const buf = await crypto.subtle.digest('SHA-256', data) | |
| return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2,'0')).join('') | |
| } | |
| async function wormAppend(type, agent, payload) { | |
| const prev = wormChain.length ? wormChain[wormChain.length-1].hash : 'GENESIS' | |
| const body = { type, agent: agent || null, payload, ts: Date.now(), prev, block: wormChain.length } | |
| const hash = await sha256(JSON.stringify(body) + prev) | |
| const entry = { ...body, hash } | |
| wormChain.push(entry) | |
| document.getElementById('worm-count').textContent = String(wormChain.length) | |
| logLocal('worm.seal', `${type} #${entry.block} ${hash.slice(0,12)}…`, 'info') | |
| if (screen === 'worm') renderWorm() | |
| return entry | |
| } | |
| async function wormVerify() { | |
| if (!wormChain.length) return { ok:true, msg:'empty chain — valid' } | |
| let prev = 'GENESIS' | |
| for (let i = 0; i < wormChain.length; i++) { | |
| const e = wormChain[i] | |
| if (e.prev !== prev) return { ok:false, msg:`break at block ${i}: prev mismatch` } | |
| const body = { type:e.type, agent:e.agent, payload:e.payload, ts:e.ts, prev:e.prev, block:e.block } | |
| const expect = await sha256(JSON.stringify(body) + e.prev) | |
| if (expect !== e.hash) return { ok:false, msg:`break at block ${i}: hash mismatch` } | |
| prev = e.hash | |
| } | |
| return { ok:true, msg:`chain OK · ${wormChain.length} blocks` } | |
| } | |
| async function clearLedger() { | |
| wormChain = [] | |
| document.getElementById('worm-count').textContent = '0' | |
| await wormAppend('genesis', null, { note:'testnet ledger reset' }) | |
| logLocal('worm.clear', 'ledger cleared (testnet)', 'warn') | |
| if (screen === 'worm') renderWorm() | |
| } | |
| // ── Constitution (Prolog-style) ──────────────────────────────────────────────── | |
| const CONSTITUTION = { | |
| facts: [ | |
| 'agent(A, Trust, Domain).', | |
| 'action(Name, Risk, MinTrust, Domain).', | |
| ], | |
| actions: { | |
| chat: { risk:'low', minTrust:'low', domain:'any' }, | |
| browse: { risk:'medium', minTrust:'medium', domain:'any' }, | |
| bash: { risk:'high', minTrust:'high', domain:'any' }, | |
| bash_read: { risk:'low', minTrust:'low', domain:'any' }, | |
| bash_write: { risk:'high', minTrust:'high', domain:'core' }, | |
| dump_world: { risk:'medium', minTrust:'medium', domain:'any' }, | |
| clear_ledger:{ risk:'high', minTrust:'root', domain:'any' }, | |
| audit_scan: { risk:'low', minTrust:'medium', domain:'security' }, | |
| }, | |
| rules: [ | |
| { pred:'can_execute(Agent, Action)', body:'trust_gte(Agent.trust, Action.minTrust), domain_ok(Agent.domain, Action.domain), resonance_ok(Agent), risk_gate(Action.risk, Agent.trust)' }, | |
| { pred:'trust_gte(T, Min)', body:'rank(T) >= rank(Min)' }, | |
| { pred:'domain_ok(D, Need)', body:'Need = any OR D = Need OR D = any' }, | |
| { pred:'resonance_ok(Agent)', body:'gate_scores(Agent).resonance >= 50' }, | |
| { pred:'risk_gate(high, Trust)', body:'Trust in {high, root} OR council_vote (>=3/6)' }, | |
| ] | |
| } | |
| function agentGates(a) { | |
| // Deterministic pseudo-scores from key + tick | |
| const seed = [...a.key].reduce((s,c)=>s+c.charCodeAt(0),0) + simTick | |
| const opsec = a.group === 'SECURITY' || a.trust === 'root' || a.trust === 'high' ? 90 + (seed%10) : 70 + (seed%20) | |
| const resonance = 55 + ((seed * 7) % 40) | |
| const trust = TRUST_RANK[a.trust] * 24 + (seed % 8) | |
| const entropy = a.group === 'CHAOS' ? 40 + (seed%40) : 5 + (seed%15) | |
| return { | |
| OPSEC: Math.min(100, opsec), | |
| Resonance: Math.min(100, resonance), | |
| Trust: Math.min(100, trust), | |
| Entropy: Math.min(100, entropy), | |
| } | |
| } | |
| function evaluateConstitution(agent, actionName) { | |
| const action = CONSTITUTION.actions[actionName] | |
| if (!action) return { approved:false, reason:`unknown_action(${actionName})`, checks:[] } | |
| const gates = agentGates(agent) | |
| const checks = [] | |
| const trustOk = TRUST_RANK[agent.trust] >= TRUST_RANK[action.minTrust] | |
| checks.push({ name:`trust_gte(${agent.trust}, ${action.minTrust})`, ok: trustOk }) | |
| const domainOk = action.domain === 'any' || agent.domain === 'any' || agent.domain === action.domain | |
| checks.push({ name:`domain_ok(${agent.domain}, ${action.domain})`, ok: domainOk }) | |
| const resOk = gates.Resonance >= 50 | |
| checks.push({ name:`resonance_ok(${gates.Resonance})`, ok: resOk }) | |
| let riskOk = true | |
| if (action.risk === 'high' && TRUST_RANK[agent.trust] < TRUST_RANK.high) { | |
| // low-trust high-risk: denied without council (emulated deny) | |
| riskOk = false | |
| checks.push({ name:`risk_gate(high, ${agent.trust}) → council required`, ok: false }) | |
| } else { | |
| checks.push({ name:`risk_gate(${action.risk}, ${agent.trust})`, ok: true }) | |
| } | |
| const approved = trustOk && domainOk && resOk && riskOk | |
| return { approved, reason: approved ? 'APPROVED' : 'DENIED', checks, seal: null, action, gates } | |
| } | |
| async function proposeAndExecute(agent, actionName, payload, executor) { | |
| const verdict = evaluateConstitution(agent, actionName) | |
| const sealBody = { agent: agent.key, action: actionName, payload, verdict: verdict.reason, checks: verdict.checks } | |
| const entry = await wormAppend('proposal', agent.key, sealBody) | |
| verdict.seal = entry.hash | |
| logLocal('constitution.eval', `${agent.label} ${actionName} → ${verdict.reason}`, verdict.approved ? 'info' : 'error') | |
| if (!verdict.approved) { | |
| // Check if council can override for high-risk actions | |
| const action = CONSTITUTION.actions[actionName] | |
| if (action && action.risk === 'high' && councilMembers.length >= 3) { | |
| // Trigger council vote for high-risk denied actions | |
| const voteId = `vote_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` | |
| const voteData = { | |
| voteId, | |
| agent: agent.key, | |
| action: actionName, | |
| payload, | |
| verdict, | |
| seal: entry.hash, | |
| created: Date.now(), | |
| votes: {}, | |
| executor: executor | |
| } | |
| pendingVotes[voteId] = voteData | |
| // Show voting modal | |
| showCouncilVote(voteData) | |
| // Set timeout for vote (5 minutes) | |
| voteTimers[voteId] = setTimeout(() => { | |
| resolveVote(voteId, false, 'timeout') | |
| }, 300000) | |
| // Return pending state | |
| return { ok:'pending', verdict, result: null, voteId } | |
| } | |
| await wormAppend('deny', agent.key, { action: actionName, reason: verdict.reason, seal: entry.hash }) | |
| return { ok:false, verdict, result: null } | |
| } | |
| let result = null | |
| try { | |
| result = await executor() | |
| await wormAppend('execute', agent.key, { action: actionName, payload, resultSummary: String(result).slice(0,200), seal: entry.hash }) | |
| } catch (e) { | |
| await wormAppend('error', agent.key, { action: actionName, error: String(e.message || e) }) | |
| return { ok:false, verdict, result: null, error: e } | |
| } | |
| return { ok:true, verdict, result } | |
| } | |
| // ── Logcat ───────────────────────────────────────────────────────────────────── | |
| function logLocal(subject, payload, level) { | |
| const ts = new Date().toLocaleTimeString('en-US',{hour12:false}) | |
| const entry = document.createElement('div') | |
| entry.className = 'log-entry' | |
| entry.innerHTML = `<span class="log-ts">${ts}</span> <span class="log-subject ${level||''}">${subject}</span> <span class="log-payload">${String(payload).slice(0,160)}</span>` | |
| const body = document.getElementById('logcat-body') | |
| body.insertBefore(entry, body.firstChild) | |
| while (body.children.length > 100) body.removeChild(body.lastChild) | |
| document.getElementById('nats-status').textContent = '● local + mesh' | |
| document.getElementById('nats-status').style.color = '#00ff88' | |
| } | |
| // ── Clock / chrome ───────────────────────────────────────────────────────────── | |
| function drawClock() { | |
| const t = new Date() | |
| document.getElementById('clock').textContent = | |
| String(t.getHours()).padStart(2,'0') + ':' + String(t.getMinutes()).padStart(2,'0') | |
| } | |
| drawClock(); setInterval(drawClock, 1000) | |
| function drawSignal() { | |
| const c = document.getElementById('signal-canvas') | |
| const ctx = c.getContext('2d') | |
| ctx.clearRect(0,0,22,14) | |
| for (let i=1;i<=4;i++) { | |
| const h = 3 + i*2 | |
| ctx.fillStyle = '#e8e8f0' | |
| ctx.beginPath(); ctx.roundRect(i*4-2,14-h,3,h,1); ctx.fill() | |
| } | |
| } | |
| drawSignal() | |
| function drawBattery(pct) { | |
| const c = document.getElementById('battery-canvas') | |
| const ctx = c.getContext('2d') | |
| ctx.clearRect(0,0,22,10) | |
| ctx.strokeStyle = '#e8e8f0'; ctx.lineWidth = 1 | |
| ctx.beginPath(); ctx.roundRect(0,1,18,8,2); ctx.stroke() | |
| ctx.fillStyle = '#333'; ctx.fillRect(19,3,2,4) | |
| ctx.fillStyle = pct > 20 ? '#00ff88' : '#ff453a' | |
| ctx.beginPath(); ctx.roundRect(1,2,Math.round(16*pct/100),6,1); ctx.fill() | |
| } | |
| drawBattery(87) | |
| function updateSimBadge() { | |
| const el = document.getElementById('sim-badge') | |
| if (simPaused) { | |
| el.className = 'paused' | |
| el.textContent = `SIM ■ PAUSE · t=${simTick}` | |
| } else { | |
| el.className = 'running' | |
| el.textContent = `SIM ● RUN · t=${simTick}` | |
| } | |
| } | |
| // ── UI builders ──────────────────────────────────────────────────────────────── | |
| function buildTree() { | |
| const tree = document.getElementById('tree') | |
| tree.innerHTML = '' | |
| GROUPS.forEach(g => { | |
| const groupAgents = AGENTS.filter(a => a.group === g) | |
| if (!groupAgents.length) return | |
| const hdr = document.createElement('div') | |
| hdr.className = 'group-header' + (expanded[g] ? ' open' : '') | |
| hdr.innerHTML = `<span class="arrow">▶</span>${g}<span class="count">${groupAgents.length}</span>` | |
| hdr.addEventListener('click', () => { expanded[g] = !expanded[g]; buildTree() }) | |
| tree.appendChild(hdr) | |
| if (expanded[g]) { | |
| groupAgents.forEach(a => { | |
| const row = document.createElement('div') | |
| row.className = 'agent-row' + (activeAgent.key===a.key&&screen==='chat'?' active':'') + ((messages[a.key]?.length)?' has-msgs':'') | |
| row.dataset.key = a.key | |
| row.style.setProperty('--agent-color', a.color) | |
| row.innerHTML = `<span style="font-size:12px">${a.icon}</span><span class="name">${a.label}</span><span class="dot"></span>` | |
| row.addEventListener('click', () => openChat(a)) | |
| tree.appendChild(row) | |
| }) | |
| } | |
| }) | |
| } | |
| function buildHome() { | |
| const grid = document.getElementById('app-grid') | |
| grid.innerHTML = '' | |
| SYSTEM_APPS.forEach(app => { | |
| const div = document.createElement('div') | |
| div.className = 'app-icon' | |
| div.innerHTML = ` | |
| <div class="icon-wrap" style="background:color-mix(in srgb,${app.color} 15%,transparent);border:1px solid color-mix(in srgb,${app.color} 40%,transparent)">${app.icon}</div> | |
| <span>${app.label}</span>` | |
| div.addEventListener('click', () => openApp(app.action)) | |
| grid.appendChild(div) | |
| }) | |
| HOME_KEYS.forEach(k => { | |
| const a = AGENTS.find(x=>x.key===k) | |
| if (!a) return | |
| const div = document.createElement('div') | |
| div.className = 'app-icon' | |
| div.innerHTML = ` | |
| <div class="icon-wrap" style="background:color-mix(in srgb,${a.color} 15%,transparent);border:1px solid color-mix(in srgb,${a.color} 40%,transparent)">${a.icon}</div> | |
| <span>${a.label}</span>` | |
| div.addEventListener('click', () => openChat(a)) | |
| grid.appendChild(div) | |
| }) | |
| } | |
| function buildInspector() { | |
| const a = activeAgent | |
| document.getElementById('inspector-icon').style.cssText = `width:36px;height:36px;border-radius:10px;background:color-mix(in srgb,${a.color} 15%,transparent);border:1px solid ${a.color};display:flex;align-items:center;justify-content:center;font-size:18px` | |
| document.getElementById('inspector-icon').textContent = a.icon | |
| document.getElementById('inspector-name').style.cssText = `font-size:11px;font-weight:700;color:${a.color}` | |
| document.getElementById('inspector-name').textContent = a.label | |
| document.getElementById('inspector-role').textContent = a.role | |
| document.getElementById('inspector-group').textContent = `Group: ${a.group} · trust:${a.trust}` | |
| const gates = agentGates(a) | |
| const props = [ | |
| ['Key', a.key],['Status','ACTIVE'],['Trust', a.trust.toUpperCase()], | |
| ['Domain', a.domain],['Protocol','SOVEREIGN/1.1'], | |
| ['Deed','WORM-sealed'],['Messages', String(messages[a.key]?.length??0)], | |
| ['WORM', String(wormChain.length)],['Tick', String(simTick)] | |
| ] | |
| document.getElementById('props').innerHTML = props.map(([k,v]) => | |
| `<div class="prop-row"><span class="prop-key">${k}</span><span class="prop-val${k==='Status'?' online':''}">${v}</span></div>` | |
| ).join('') | |
| const gateColors = { OPSEC:'#00ff88', Resonance:'#bf5af2', Trust:'#0a84ff', Entropy:'#ff453a' } | |
| document.getElementById('gates').innerHTML = Object.entries(gates).map(([label,val]) => | |
| `<div class="gate-row"> | |
| <div class="gate-header"><span class="gate-label">${label}</span><span class="gate-val" style="color:${gateColors[label]}">${val}%</span></div> | |
| <div class="gate-bar-bg"><div class="gate-bar-fill" style="width:${val}%;background:${gateColors[label]}"></div></div> | |
| </div>` | |
| ).join('') | |
| document.getElementById('agent-count').textContent = `ALL AGENTS (${AGENTS.length})` | |
| document.getElementById('agent-grid-icons').innerHTML = AGENTS.map(ag => | |
| `<div class="grid-icon" title="${ag.label} — ${ag.role}" | |
| style="background:color-mix(in srgb,${ag.color} ${activeAgent.key===ag.key?'25':'8'}%,transparent);border:1px solid color-mix(in srgb,${ag.color} ${activeAgent.key===ag.key?'100':'20'}%,transparent)" | |
| data-key="${ag.key}">${ag.icon}</div>` | |
| ).join('') | |
| document.getElementById('agent-grid-icons').querySelectorAll('.grid-icon').forEach(el => { | |
| el.addEventListener('click', () => openChat(AGENTS.find(a=>a.key===el.dataset.key))) | |
| }) | |
| document.getElementById('phone').style.boxShadow = | |
| `0 0 0 2px #333,0 0 0 4px #222,0 30px 80px rgba(0,0,0,.8),0 0 40px ${a.color}33` | |
| } | |
| function buildTermAgentSelect() { | |
| const sel = document.getElementById('term-agent-sel') | |
| sel.innerHTML = AGENTS.map(a => | |
| `<option value="${a.key}" ${a.key===activeAgent.key?'selected':''}>${a.label}</option>` | |
| ).join('') | |
| } | |
| // ── Screens ──────────────────────────────────────────────────────────────────── | |
| function showScreen(s) { | |
| lastScreen = screen | |
| screen = s | |
| const map = { | |
| home: 'home-screen', | |
| chat: 'chat-screen', | |
| browser: 'browser-screen', | |
| term: 'term-screen', | |
| worm: 'worm-screen', | |
| const: 'const-screen', | |
| vm: 'vm-editor-screen', | |
| } | |
| Object.values(map).forEach(id => { | |
| const el = document.getElementById(id) | |
| if (el) el.style.display = 'none' | |
| }) | |
| const show = document.getElementById(map[s]) | |
| if (show) show.style.display = 'flex' | |
| // Handle modal visibility | |
| if (s !== 'vote') { | |
| document.getElementById('council-vote-modal').style.display = 'none' | |
| } | |
| } | |
| function goHome() { | |
| showScreen('home') | |
| buildTree() | |
| } | |
| function openApp(name) { | |
| if (name === 'browser') openBrowser() | |
| else if (name === 'term') openTerm() | |
| else if (name === 'worm') openWorm() | |
| else if (name === 'const') openConstitution() | |
| else if (name === 'vm') openVM() | |
| else if (name === 'home') goHome() | |
| } | |
| function openVM() { | |
| showScreen('vm') | |
| buildVMEditor() | |
| logLocal('app.vm', 'opened VM editor', 'info') | |
| } | |
| // Council Voting Functions | |
| function showCouncilVote(voteData) { | |
| const modal = document.getElementById('council-vote-modal') | |
| const agent = AGENTS.find(a => a.key === voteData.agent) | |
| document.getElementById('vote-agent').textContent = agent ? `${agent.label} (${agent.role})` : voteData.agent | |
| document.getElementById('vote-action').textContent = voteData.action | |
| document.getElementById('vote-payload').textContent = JSON.stringify(voteData.payload).slice(0, 200) | |
| document.getElementById('vote-required').textContent = '3' | |
| document.getElementById('vote-total').textContent = String(councilMembers.length) | |
| // Setup vote buttons | |
| document.getElementById('vote-approve-btn').onclick = () => { | |
| castVote(voteData.voteId, true) | |
| modal.style.display = 'none' | |
| } | |
| document.getElementById('vote-deny-btn').onclick = () => { | |
| castVote(voteData.voteId, false) | |
| modal.style.display = 'none' | |
| } | |
| document.getElementById('vote-close-btn').onclick = () => { | |
| modal.style.display = 'none' | |
| } | |
| modal.style.display = 'flex' | |
| renderVoteTally(voteData.voteId) | |
| } | |
| function castVote(voteId, approve) { | |
| const voteData = pendingVotes[voteId] | |
| if (!voteData) return | |
| const votingAgent = activeAgent | |
| voteData.votes[votingAgent.key] = { | |
| agent: votingAgent.key, | |
| vote: approve, | |
| ts: Date.now() | |
| } | |
| renderVoteTally(voteId) | |
| // Check if vote is complete | |
| const voteCount = Object.keys(voteData.votes).length | |
| if (voteCount >= 3) { | |
| const yesVotes = Object.values(voteData.votes).filter(v => v.vote).length | |
| resolveVote(voteId, yesVotes >= 3, yesVotes >= 3 ? 'approved' : 'denied') | |
| } | |
| } | |
| function renderVoteTally(voteId) { | |
| const voteData = pendingVotes[voteId] | |
| if (!voteData) return | |
| const tally = document.getElementById('vote-tally') | |
| tally.innerHTML = '' | |
| Object.entries(voteData.votes).forEach(([agentKey, vote]) => { | |
| const agent = AGENTS.find(a => a.key === agentKey) | |
| const div = document.createElement('div') | |
| div.style = 'display:flex;align-items:center;gap:8px;font-size:9px;padding:4px 8px;background:#1e1e2a;border-radius:4px' | |
| div.innerHTML = ` | |
| <span style="color:${agent ? agent.color : '#888'}">${agent ? agent.icon : '?'}</span> | |
| <span style="color:#e8e8f0">${agent ? agent.label : agentKey}</span> | |
| <span style="margin-left:auto;color:${vote.vote ? '#00ff88' : '#ff453a'}">${vote.vote ? 'APPROVE' : 'DENY'}</span> | |
| ` | |
| tally.appendChild(div) | |
| }) | |
| // Show remaining voters | |
| const votedKeys = Object.keys(voteData.votes) | |
| councilMembers.forEach(memberKey => { | |
| if (!votedKeys.includes(memberKey)) { | |
| const agent = AGENTS.find(a => a.key === memberKey) | |
| if (agent) { | |
| const div = document.createElement('div') | |
| div.style = 'display:flex;align-items:center;gap:8px;font-size:9px;padding:4px 8px;background:#0a0a14;border-radius:4px;opacity:0.6' | |
| div.innerHTML = ` | |
| <span style="color:${agent.color}">${agent.icon}</span> | |
| <span style="color:#888">${agent.label} - pending</span> | |
| ` | |
| tally.appendChild(div) | |
| } | |
| } | |
| }) | |
| } | |
| function resolveVote(voteId, approved, reason) { | |
| const voteData = pendingVotes[voteId] | |
| if (!voteData) return | |
| // Clear timer | |
| if (voteTimers[voteId]) { | |
| clearTimeout(voteTimers[voteId]) | |
| delete voteTimers[voteId] | |
| } | |
| // Remove from pending | |
| delete pendingVotes[voteId] | |
| // Close modal | |
| document.getElementById('council-vote-modal').style.display = 'none' | |
| if (approved) { | |
| // Execute the action | |
| voteData.executor().then(result => { | |
| wormAppend('council_approve', voteData.agent, { | |
| action: voteData.action, | |
| payload: voteData.payload, | |
| voteId, | |
| reason: 'Council approved', | |
| votes: voteData.votes | |
| }) | |
| logLocal('council.vote', `${voteData.action} APPROVED by council`, 'info') | |
| }).catch(e => { | |
| wormAppend('council_error', voteData.agent, { | |
| action: voteData.action, | |
| error: String(e.message || e) | |
| }) | |
| logLocal('council.vote', `${voteData.action} FAILED: ${e.message || e}`, 'error') | |
| }) | |
| } else { | |
| // Deny the action | |
| wormAppend('council_deny', voteData.agent, { | |
| action: voteData.action, | |
| payload: voteData.payload, | |
| voteId, | |
| reason: `Council denied (${reason})`, | |
| votes: voteData.votes | |
| }) | |
| logLocal('council.vote', `${voteData.action} DENIED by council`, 'warn') | |
| } | |
| } | |
| // VM Editor Functions | |
| function buildVMEditor() { | |
| // Populate agent select | |
| const agentSel = document.getElementById('vm-agent-select') | |
| agentSel.innerHTML = AGENTS.map(a => | |
| `<option value="${a.key}" ${a.key === activeAgent.key ? 'selected' : ''}>${a.label}</option>` | |
| ).join('') | |
| // Populate council configuration | |
| buildCouncilConfig() | |
| // Load current state | |
| document.getElementById('vm-tick-input').value = simTick | |
| document.getElementById('vm-paused-select').value = String(simPaused) | |
| document.getElementById('vm-worm-input').value = wormChain.length | |
| // VFS path input | |
| document.getElementById('vfs-path-input').value = '/' | |
| // Event listeners | |
| document.getElementById('vm-save-btn').onclick = saveVMState | |
| document.getElementById('vfs-read-btn').onclick = readVFSFile | |
| document.getElementById('vfs-write-btn').onclick = writeVFSFile | |
| document.getElementById('vfs-new-btn').onclick = newVFSFile | |
| document.getElementById('council-add-btn').onclick = addCouncilMember | |
| // Load file list | |
| loadVFSList() | |
| } | |
| function saveVMState() { | |
| // Save VM state | |
| const newTick = parseInt(document.getElementById('vm-tick-input').value) || 0 | |
| const newPaused = document.getElementById('vm-paused-select').value === 'true' | |
| const newAgentKey = document.getElementById('vm-agent-select').value | |
| const newWormCount = parseInt(document.getElementById('vm-worm-input').value) || 0 | |
| simTick = newTick | |
| simPaused = newPaused | |
| const newAgent = AGENTS.find(a => a.key === newAgentKey) | |
| if (newAgent) activeAgent = newAgent | |
| // WORM count - just informational | |
| updateSimBadge() | |
| buildTree() | |
| buildInspector() | |
| logLocal('vm.save', `state saved: tick=${simTick} paused=${simPaused} agent=${activeAgent.key}`, 'info') | |
| } | |
| function loadVFSList() { | |
| // Load VFS structure into a display | |
| // This is a placeholder - would be enhanced with actual file browser | |
| } | |
| function readVFSFile() { | |
| const path = document.getElementById('vfs-path-input').value | |
| const content = getVFSContent(path) | |
| document.getElementById('vfs-file-editor').value = content | |
| logLocal('vfs.read', `read ${path}`, 'info') | |
| } | |
| function writeVFSFile() { | |
| const path = document.getElementById('vfs-path-input').value | |
| const content = document.getElementById('vfs-file-editor').value | |
| setVFSContent(path, content) | |
| logLocal('vfs.write', `wrote ${path}`, 'info') | |
| } | |
| function newVFSFile() { | |
| const path = document.getElementById('vfs-path-input').value | |
| document.getElementById('vfs-file-editor').value = '' | |
| logLocal('vfs.new', `new file ${path}`, 'info') | |
| } | |
| function getVFSContent(path) { | |
| // Get content from VFS | |
| if (path === '/etc/hostname') return 'sovereign-emulator' | |
| if (path === '/etc/version') return 'v∞.1' | |
| if (path === '/worm/count.txt') return String(wormChain.length) | |
| if (path === '/worm/head.txt') { | |
| const h = wormChain[wormChain.length-1] | |
| return h ? h.hash : 'GENESIS' | |
| } | |
| if (path === '/constitution/rules.pl') { | |
| return CONSTITUTION.rules.map(r => `${r.pred} :- ${r.body}.`).join('\n') | |
| } | |
| if (path === '/constitution/can_execute.pl') { | |
| return 'can_execute(Agent, Action) :-\n trust_gte, domain_ok, resonance_ok, risk_gate.' | |
| } | |
| if (path.startsWith('/agents/')) { | |
| const key = path.replace('/agents/', '').replace('.json', '') | |
| const a = AGENTS.find(x => x.key === key) | |
| return a ? JSON.stringify(a, null, 2) : 'Not found' | |
| } | |
| return `Path: ${path}\nContent: (use cat command in terminal)` | |
| } | |
| function setVFSContent(path, content) { | |
| // Set content in VFS | |
| // For now, just log - would need to extend VFS | |
| logLocal('vfs.set', `set ${path} = ${content.slice(0, 50)}...`, 'info') | |
| } | |
| // Council Configuration Functions | |
| function buildCouncilConfig() { | |
| const availableSel = document.getElementById('council-available') | |
| availableSel.innerHTML = AGENTS.map(a => | |
| `<option value="${a.key}">${a.label} (${a.role})</option>` | |
| ).join('') | |
| const councilList = document.getElementById('council-list') | |
| councilList.innerHTML = councilMembers.map(memberKey => { | |
| const agent = AGENTS.find(a => a.key === memberKey) | |
| if (agent) { | |
| return ` | |
| <div style="background:#0a0a14;border:1px solid #1e1e2a;border-radius:6px;padding:4px 8px;display:flex;align-items:center;gap:6px"> | |
| <span style="color:${agent.color}">${agent.icon}</span> | |
| <span style="font-size:9px;color:#e8e8f0">${agent.label}</span> | |
| <button type="button" onclick="removeCouncilMember('${agent.key}')" style="margin-left:auto;background:#ff453a;border:1px solid #ff453a;color:#fff;border-radius:3px;padding:1px 4px;font-size:7px;cursor:pointer">✕</button> | |
| </div> | |
| ` | |
| } | |
| return '' | |
| }).join('') | |
| } | |
| function addCouncilMember() { | |
| const sel = document.getElementById('council-available') | |
| const agentKey = sel.value | |
| if (!agentKey || councilMembers.includes(agentKey)) return | |
| councilMembers.push(agentKey) | |
| buildCouncilConfig() | |
| logLocal('council.add', `added ${agentKey}`, 'info') | |
| } | |
| function removeCouncilMember(agentKey) { | |
| councilMembers = councilMembers.filter(k => k !== agentKey) | |
| buildCouncilConfig() | |
| logLocal('council.remove', `removed ${agentKey}`, 'info') | |
| } | |
| function openChat(a) { | |
| activeAgent = a | |
| document.getElementById('incoming-call').style.display = 'none' | |
| document.getElementById('chat-back').style.color = a.color | |
| document.getElementById('chat-agent-icon').style.cssText = `width:28px;height:28px;border-radius:50%;background:color-mix(in srgb,${a.color} 15%,transparent);border:1px solid ${a.color};display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0` | |
| document.getElementById('chat-agent-icon').textContent = a.icon | |
| document.getElementById('chat-agent-name').style.cssText = `font-size:11px;font-weight:700;color:${a.color}` | |
| document.getElementById('chat-agent-name').textContent = a.label | |
| document.getElementById('chat-agent-role').textContent = a.role | |
| document.getElementById('send-btn').style.background = a.color | |
| document.getElementById('msg-input').placeholder = `Message ${a.label}… /browse /bash` | |
| document.getElementById('empty-icon').textContent = a.icon | |
| document.getElementById('empty-text').textContent = `Open channel with ${a.label}` | |
| document.getElementById('empty-sub').style.cssText = `color:${a.color};margin-top:4px;font-size:8px` | |
| document.getElementById('empty-sub').textContent = `${a.role} · trust ${a.trust}` | |
| renderMessages() | |
| showScreen('chat') | |
| buildTree() | |
| buildInspector() | |
| buildTermAgentSelect() | |
| } | |
| function openBrowser(url) { | |
| showScreen('browser') | |
| if (url) navigateBrowser(url) | |
| else if (browserIndex < 0) navigateBrowser('https://snapkittywest.github.io/sovereign-emulator/') | |
| logLocal('app.browser', 'opened', 'info') | |
| } | |
| function openTerm() { | |
| showScreen('term') | |
| buildTermAgentSelect() | |
| document.getElementById('term-input').focus() | |
| logLocal('app.term', 'bash in/out ready', 'info') | |
| } | |
| function openWorm() { | |
| showScreen('worm') | |
| renderWorm() | |
| } | |
| function openConstitution() { | |
| showScreen('const') | |
| renderConstitution() | |
| } | |
| function renderWorm() { | |
| const list = document.getElementById('worm-list') | |
| if (!wormChain.length) { | |
| list.innerHTML = '<div style="color:#444;padding:20px;text-align:center">Empty ledger — run an action or press W</div>' | |
| return | |
| } | |
| list.innerHTML = [...wormChain].reverse().map(e => ` | |
| <div class="worm-entry"> | |
| <div class="meta">#${e.block} · ${e.type} · ${e.agent||'system'} · ${new Date(e.ts).toLocaleTimeString()}</div> | |
| <div class="body">${JSON.stringify(e.payload).slice(0,180)}</div> | |
| <div class="hash">${e.hash}</div> | |
| </div> | |
| `).join('') | |
| } | |
| function renderConstitution() { | |
| const list = document.getElementById('const-list') | |
| const actionRows = Object.entries(CONSTITUTION.actions).map(([name, a]) => | |
| `<div class="const-rule"><div class="pred">action(${name}, ${a.risk}, ${a.minTrust}, ${a.domain}).</div></div>` | |
| ).join('') | |
| const ruleRows = CONSTITUTION.rules.map(r => | |
| `<div class="const-rule"><div class="pred">${r.pred} :-</div><div class="body">${r.body}.</div></div>` | |
| ).join('') | |
| const a = activeAgent | |
| const sample = evaluateConstitution(a, 'bash') | |
| const checks = sample.checks.map(c => | |
| `<div style="margin:2px 0"><span class="badge ${c.ok?'ok':'deny'}">${c.ok?'✓':'✗'}</span> ${c.name}</div>` | |
| ).join('') | |
| list.innerHTML = ` | |
| <div style="margin-bottom:10px;color:#888">Live probe: can_execute(${a.key}, bash)? | |
| <span class="badge ${sample.approved?'ok':'deny'}">${sample.reason}</span> | |
| </div> | |
| ${checks} | |
| <div class="section-label" style="margin:12px 0 6px">Rules</div> | |
| ${ruleRows} | |
| <div class="section-label" style="margin:12px 0 6px">Actions</div> | |
| ${actionRows} | |
| ` | |
| } | |
| // ── Browser ──────────────────────────────────────────────────────────────────── | |
| function setBrowserStatus(t) { | |
| document.getElementById('browser-status').textContent = t | |
| } | |
| async function navigateBrowser(raw, push=true) { | |
| let url = (raw || '').trim() | |
| if (!url) return | |
| if (!/^https?:\/\//i.test(url) && !url.startsWith('about:')) { | |
| if (url.includes('.') && !url.includes(' ')) url = 'https://' + url | |
| else url = 'https://duckduckgo.com/?q=' + encodeURIComponent(url) | |
| } | |
| document.getElementById('browser-url').value = url | |
| setBrowserStatus('loading…') | |
| const frame = document.getElementById('browser-frame') | |
| const fallback = document.getElementById('browser-fallback') | |
| fallback.style.display = 'none' | |
| frame.style.display = 'block' | |
| if (push) { | |
| browserHistory = browserHistory.slice(0, browserIndex + 1) | |
| browserHistory.push(url) | |
| browserIndex = browserHistory.length - 1 | |
| } | |
| // Attempt iframe load | |
| try { | |
| frame.src = url | |
| } catch {} | |
| // Also fetch text snapshot for agent / when iframe blocked | |
| try { | |
| const res = await fetch(CORS_PROXY + encodeURIComponent(url), { signal: AbortSignal.timeout(12000) }) | |
| const text = await res.text() | |
| const clean = text.replace(/<script[\s\S]*?<\/script>/gi,'') | |
| .replace(/<style[\s\S]*?<\/style>/gi,'') | |
| .replace(/<[^>]+>/g,' ') | |
| .replace(/\s+/g,' ') | |
| .trim() | |
| .slice(0, 4000) | |
| document.getElementById('fallback-url').textContent = url | |
| document.getElementById('fallback-body').textContent = clean || '(empty or blocked body)' | |
| setBrowserStatus(`ok · ${clean.length} chars snapshot · iframe may be blocked by site`) | |
| // After short delay, if iframe failed we still have fallback available | |
| setTimeout(() => { | |
| // keep both available — user can toggle via reload showing fallback if needed | |
| }, 2000) | |
| return clean | |
| } catch (e) { | |
| setBrowserStatus('fetch failed (CORS/network) · iframe may still work') | |
| document.getElementById('fallback-url').textContent = url | |
| document.getElementById('fallback-body').textContent = `Could not fetch snapshot: ${e.message}\nIframe navigation attempted for ${url}` | |
| fallback.style.display = 'flex' | |
| frame.style.display = 'none' | |
| return null | |
| } | |
| } | |
| async function agentBrowse(agent, url) { | |
| return proposeAndExecute(agent, 'browse', { url }, async () => { | |
| openBrowser() | |
| const snap = await navigateBrowser(url) | |
| return snap ? snap.slice(0, 500) : `navigated:${url}` | |
| }) | |
| } | |
| // ── Terminal (bash in / bash out) ────────────────────────────────────────────── | |
| function termPrint(kind, text) { | |
| const out = document.getElementById('term-out') | |
| const line = document.createElement('div') | |
| line.className = kind | |
| line.textContent = text | |
| out.appendChild(line) | |
| out.scrollTop = out.scrollHeight | |
| } | |
| function termActingAgent() { | |
| const key = document.getElementById('term-agent-sel').value | |
| return AGENTS.find(a => a.key === key) || activeAgent | |
| } | |
| function classifyBash(cmd) { | |
| const c = cmd.trim().split(/\s+/)[0] | |
| const read = new Set(['help','clear','echo','date','whoami','agents','ls','cat','pwd','status','worm','constitution','const','gates','history','id','uname']) | |
| const write = new Set(['rm','touch','write','export','import','reset']) | |
| if (read.has(c) || c === '') return 'bash_read' | |
| if (write.has(c)) return 'bash_write' | |
| return 'bash' | |
| } | |
| async function runBashLine(rawCmd, agent) { | |
| const cmd = rawCmd.trim() | |
| if (!cmd) return | |
| termPrint('in', `$ ${cmd}`) | |
| logLocal('bash.in', cmd, 'info') | |
| const action = classifyBash(cmd) | |
| const result = await proposeAndExecute(agent, action, { cmd }, async () => { | |
| const out = await execVirtualShell(cmd, agent) | |
| return out | |
| }) | |
| if (!result.ok) { | |
| const deny = `DENIED by constitution: ${result.verdict.reason}` | |
| termPrint('err', deny) | |
| result.verdict.checks.forEach(c => termPrint('sys', ` ${c.ok?'✓':'✗'} ${c.name}`)) | |
| logLocal('bash.deny', deny, 'error') | |
| return deny | |
| } | |
| const lines = String(result.result).split('\n') | |
| lines.forEach(l => termPrint('out', l)) | |
| logLocal('bash.out', String(result.result).slice(0,120), 'info') | |
| return result.result | |
| } | |
| async function execVirtualShell(cmd, agent) { | |
| // Input validation and security hardening | |
| if (!cmd || typeof cmd !== 'string') { | |
| return 'Error: Invalid command' | |
| } | |
| // Block potentially dangerous patterns | |
| const dangerousPatterns = [ | |
| /<script/i, /javascript:/i, /onerror=/i, /onload=/i, | |
| /\$\{/i, /\${\{/i, /eval\s*\(/i, /Function\s*\(/i, | |
| /<iframe/i, /<object/i, /<embed/i, /document\.write/i | |
| ] | |
| for (const pattern of dangerousPatterns) { | |
| if (pattern.test(cmd)) { | |
| logLocal('bash.security', `Blocked dangerous command: ${cmd.slice(0, 50)}`, 'error') | |
| return 'Error: Command contains disallowed pattern' | |
| } | |
| } | |
| const parts = cmd.match(/(?:[^\s"]+|"[^"]*")+/g)?.map(s => s.replace(/^"|"$/g,'')) || [] | |
| const c = parts[0] | |
| const args = parts.slice(1) | |
| switch (c) { | |
| case 'help': | |
| return [ | |
| 'Sovereign virtual bash — constitution-gated', | |
| ' help this text', | |
| ' clear clear terminal', | |
| ' echo <text> print', | |
| ' date | whoami | id identity', | |
| ' agents list agents', | |
| ' ls [path] virtual FS', | |
| ' cat <path> read file', | |
| ' status | gates runtime status', | |
| ' worm [verify|dump] ledger ops', | |
| ' constitution show rules', | |
| ' browse <url> open browser (agent)', | |
| ' fetch <url> fetch page text', | |
| ' msg <agent> <text> message agent', | |
| ' tick step sim clock', | |
| ' export dump session JSON', | |
| ].join('\n') | |
| case 'clear': | |
| document.getElementById('term-out').innerHTML = '' | |
| return '' | |
| case 'echo': | |
| return args.join(' ') | |
| case 'date': | |
| return new Date().toISOString() | |
| case 'whoami': | |
| case 'id': | |
| return `${agent.key} uid=${agent.trust} gid=${agent.group} domain=${agent.domain}` | |
| case 'uname': | |
| return 'SnapKittyOS sovereign-emulator 1.1 browser-vm' | |
| case 'pwd': | |
| return '/sovereign' | |
| case 'agents': | |
| return AGENTS.map(a => `${a.key.padEnd(12)} ${a.trust.padEnd(8)} ${a.group.padEnd(10)} ${a.role}`).join('\n') | |
| case 'ls': { | |
| const path = args[0] || '/' | |
| if (path === '/agents' || path === '/agents/') { | |
| return AGENTS.map(a => a.key).join(' ') | |
| } | |
| const list = VFS[path] || VFS[path.endsWith('/') ? path : path + '/'] | |
| if (list) return list.join(' ') | |
| if (path.startsWith('/agents/')) return AGENTS.map(a => a.key + '.json').join(' ') | |
| return `ls: ${path}: No such file or directory` | |
| } | |
| case 'cat': { | |
| const path = args[0] | |
| if (!path) return 'cat: missing path' | |
| if (path === '/etc/hostname') return 'sovereign-emulator' | |
| if (path === '/etc/version') return 'v∞.1' | |
| if (path === '/worm/count.txt') return String(wormChain.length) | |
| if (path === '/worm/head.txt') { | |
| const h = wormChain[wormChain.length-1] | |
| return h ? h.hash : 'GENESIS' | |
| } | |
| if (path === '/constitution/rules.pl') { | |
| return CONSTITUTION.rules.map(r => `${r.pred} :- ${r.body}.`).join('\n') | |
| } | |
| if (path === '/constitution/can_execute.pl') { | |
| return 'can_execute(Agent, Action) :-\n trust_gte, domain_ok, resonance_ok, risk_gate.' | |
| } | |
| if (path.startsWith('/agents/')) { | |
| const key = path.replace('/agents/','').replace('.json','') | |
| const a = AGENTS.find(x => x.key === key) | |
| return a ? JSON.stringify(a, null, 2) : 'cat: not found' | |
| } | |
| return `cat: ${path}: No such file or directory` | |
| } | |
| case 'status': | |
| return `tick=${simTick} paused=${simPaused} worm=${wormChain.length} screen=${screen} agent=${agent.key}` | |
| case 'gates': { | |
| const g = agentGates(agent) | |
| return Object.entries(g).map(([k,v]) => `${k}: ${v}%`).join('\n') | |
| } | |
| case 'worm': { | |
| if (args[0] === 'verify') { | |
| const v = await wormVerify() | |
| return v.ok ? `OK ${v.msg}` : `FAIL ${v.msg}` | |
| } | |
| if (args[0] === 'dump') { | |
| await wormAppend('world_dump', agent.key, worldSnapshot()) | |
| return `dumped world @ block ${wormChain.length-1}` | |
| } | |
| return wormChain.slice(-5).map(e => `#${e.block} ${e.type} ${e.hash.slice(0,16)}…`).join('\n') || 'empty' | |
| } | |
| case 'constitution': | |
| case 'const': | |
| openConstitution() | |
| return 'opened constitution viewer' | |
| case 'browse': { | |
| const url = args[0] | |
| if (!url) return 'usage: browse <url>' | |
| const r = await agentBrowse(agent, url) | |
| return r.ok ? `browsed ${url}\n${String(r.result).slice(0,300)}` : `browse denied: ${r.verdict.reason}` | |
| } | |
| case 'fetch': { | |
| const url = args[0] | |
| if (!url) return 'usage: fetch <url>' | |
| const r = await agentBrowse(agent, url) | |
| return r.ok ? String(r.result) : `fetch denied: ${r.verdict.reason}` | |
| } | |
| case 'msg': { | |
| const key = args[0] | |
| const text = args.slice(1).join(' ') | |
| const target = AGENTS.find(a => a.key === key) | |
| if (!target || !text) return 'usage: msg <agent> <text>' | |
| if (!messages[target.key]) messages[target.key] = [] | |
| messages[target.key].push({ role:'user', text:`[bash] ${text}`, ts:Date.now() }) | |
| messages[target.key].push({ role:'agent', text: localAgentReply(target, text), ts:Date.now() }) | |
| return `messaged ${target.label}` | |
| } | |
| case 'tick': | |
| stepClock() | |
| return `tick → ${simTick}` | |
| case 'export': | |
| return JSON.stringify(worldSnapshot(), null, 2).slice(0, 2000) | |
| case 'history': | |
| return termHistory.slice(-20).join('\n') || '(empty)' | |
| default: | |
| return `sovereign-sh: ${c}: command not found (type help)` | |
| } | |
| } | |
| function worldSnapshot() { | |
| return { | |
| tick: simTick, | |
| paused: simPaused, | |
| activeAgent: activeAgent.key, | |
| wormBlocks: wormChain.length, | |
| wormHead: wormChain[wormChain.length-1]?.hash || 'GENESIS', | |
| messageCounts: Object.fromEntries(Object.entries(messages).map(([k,v]) => [k, v.length])), | |
| agents: AGENTS.length, | |
| browserUrl: browserHistory[browserIndex] || null, | |
| ts: Date.now(), | |
| } | |
| } | |
| async function onTermSubmit() { | |
| const input = document.getElementById('term-input') | |
| const cmd = input.value | |
| if (!cmd.trim()) return | |
| termHistory.push(cmd) | |
| termHistIdx = termHistory.length | |
| input.value = '' | |
| const agent = termActingAgent() | |
| await runBashLine(cmd, agent) | |
| } | |
| // ── Chat + slash commands ────────────────────────────────────────────────────── | |
| function localAgentReply(agent, text) { | |
| const lower = text.toLowerCase() | |
| if (lower.includes('browse') || lower.includes('http')) { | |
| return `${agent.label}: I can browse via constitution — use /browse <url> or open BROWSER. Domain=${agent.domain}, trust=${agent.trust}.` | |
| } | |
| if (lower.includes('bash') || lower.includes('shell') || lower.includes('terminal')) { | |
| return `${agent.label}: Bash in/out is available in the Terminal app. High-risk cmds need high trust. My trust=${agent.trust}.` | |
| } | |
| if (lower.includes('worm') || lower.includes('ledger')) { | |
| return `${agent.label}: WORM chain has ${wormChain.length} sealed blocks. Press V to verify or open WORM app.` | |
| } | |
| if (lower.includes('constitution') || lower.includes('can_execute')) { | |
| const v = evaluateConstitution(agent, 'bash') | |
| return `${agent.label}: can_execute(${agent.key}, bash)? → ${v.reason}. Open CONST for full rules.` | |
| } | |
| if (lower.includes('status') || lower.includes('who')) { | |
| return `${agent.label} · ${agent.role} · trust=${agent.trust} · domain=${agent.domain} · tick=${simTick}` | |
| } | |
| return `${agent.label} [${agent.role}]: Acknowledged — "${text.slice(0,80)}". Tools: /browse · /bash · /worm · constitution-gated runtime online.` | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById('msg-input') | |
| const text = input.value.trim() | |
| if (!text || loading) return | |
| input.value = '' | |
| loading = true | |
| updateSendBtn() | |
| const a = activeAgent | |
| if (!messages[a.key]) messages[a.key] = [] | |
| messages[a.key].push({ role:'user', text, ts:Date.now() }) | |
| renderMessages() | |
| // Slash commands | |
| if (text.startsWith('/')) { | |
| const reply = await handleSlash(a, text) | |
| messages[a.key].push({ role:'agent', text: reply, ts:Date.now() }) | |
| loading = false | |
| updateSendBtn() | |
| renderMessages() | |
| buildTree() | |
| buildInspector() | |
| return | |
| } | |
| try { | |
| await proposeAndExecute(a, 'chat', { text }, async () => 'chat') | |
| const history = messages[a.key].map(m => ({ role: m.role==='user'?'user':'assistant', content: m.text })) | |
| const res = await fetch(API, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({ agent: a.key, messages: history }) | |
| }) | |
| const data = await res.json() | |
| let reply = data.content ?? data.message ?? data.text ?? data.error | |
| if (!reply || typeof reply !== 'string') reply = localAgentReply(a, text) | |
| messages[a.key].push({ role:'agent', text: reply, ts:Date.now() }) | |
| } catch (e) { | |
| messages[a.key].push({ role:'agent', text: localAgentReply(a, text) + '\n\n(local fallback — mesh API unreachable)', ts:Date.now() }) | |
| } | |
| loading = false | |
| updateSendBtn() | |
| renderMessages() | |
| buildTree() | |
| buildInspector() | |
| } | |
| async function handleSlash(agent, text) { | |
| const [cmd, ...rest] = text.slice(1).split(/\s+/) | |
| const arg = rest.join(' ') | |
| switch (cmd.toLowerCase()) { | |
| case 'help': | |
| return 'Slash: /browse <url> · /bash <cmd> · /worm · /const · /status · /help' | |
| case 'browse': { | |
| if (!arg) return 'usage: /browse <url>' | |
| const r = await agentBrowse(agent, arg) | |
| return r.ok | |
| ? `🌐 BROWSE approved (seal ${r.verdict.seal?.slice(0,12)}…)\n${String(r.result).slice(0,600)}` | |
| : `🌐 BROWSE denied — ${r.verdict.reason}` | |
| } | |
| case 'bash': { | |
| if (!arg) return 'usage: /bash <cmd>' | |
| openTerm() | |
| const out = await runBashLine(arg, agent) | |
| return `⌨ BASH\n${out}` | |
| } | |
| case 'worm': | |
| openWorm() | |
| return `WORM ledger: ${wormChain.length} blocks. Opened viewer.` | |
| case 'const': | |
| case 'constitution': | |
| openConstitution() | |
| return 'Constitution viewer opened.' | |
| case 'status': | |
| return `tick=${simTick} paused=${simPaused} worm=${wormChain.length} agent=${agent.key}` | |
| default: | |
| return `unknown command /${cmd} — try /help` | |
| } | |
| } | |
| function renderMessages() { | |
| const a = activeAgent | |
| const msgs = messages[a.key] ?? [] | |
| const container = document.getElementById('messages') | |
| const empty = document.getElementById('empty-state') | |
| empty.style.display = msgs.length ? 'none' : 'block' | |
| container.querySelectorAll('.msg,.typing-bubble-wrap').forEach(el => el.remove()) | |
| msgs.forEach(m => { | |
| const div = document.createElement('div') | |
| div.className = 'msg ' + m.role | |
| const bub = document.createElement('div') | |
| bub.className = 'bubble ' + m.role | |
| if (m.role === 'agent') { | |
| bub.style.background = `color-mix(in srgb,${a.color} 12%,transparent)` | |
| bub.style.border = `1px solid color-mix(in srgb,${a.color} 25%,transparent)` | |
| } | |
| bub.textContent = m.text | |
| div.appendChild(bub) | |
| container.appendChild(div) | |
| }) | |
| if (loading) { | |
| const t = document.createElement('div') | |
| t.className = 'msg agent typing-bubble-wrap' | |
| const b = document.createElement('div') | |
| b.className = 'typing-bubble' | |
| b.style.cssText = `background:color-mix(in srgb,${a.color} 12%,transparent);border:1px solid color-mix(in srgb,${a.color} 25%,transparent);color:${a.color};padding:7px 12px;border-radius:12px 12px 12px 2px;font-size:12px` | |
| b.textContent = '···' | |
| t.appendChild(b); container.appendChild(t) | |
| } | |
| container.scrollTop = container.scrollHeight | |
| } | |
| function updateSendBtn() { | |
| const btn = document.getElementById('send-btn') | |
| const input = document.getElementById('msg-input') | |
| btn.disabled = loading || !input.value.trim() | |
| } | |
| // ── Simulation clock ─────────────────────────────────────────────────────────── | |
| function stepClock() { | |
| simTick++ | |
| updateSimBadge() | |
| buildInspector() | |
| logLocal('sim.tick', `t=${simTick}`, 'info') | |
| // Light agent heartbeat | |
| const idx = simTick % AGENTS.length | |
| const a = AGENTS[idx] | |
| logLocal('agent.heartbeat', `${a.label} alive · trust=${a.trust}`, 'info') | |
| } | |
| function togglePause() { | |
| simPaused = !simPaused | |
| updateSimBadge() | |
| logLocal('sim.pause', simPaused ? 'paused' : 'resumed', 'warn') | |
| } | |
| function agentTick() { | |
| stepClock() | |
| const a = AGENTS[simTick % AGENTS.length] | |
| if (!messages[a.key]) messages[a.key] = [] | |
| messages[a.key].push({ | |
| role: 'agent', | |
| text: `⏱ heartbeat t=${simTick} · gates R=${agentGates(a).Resonance}%`, | |
| ts: Date.now() | |
| }) | |
| buildTree() | |
| if (screen === 'chat' && activeAgent.key === a.key) renderMessages() | |
| } | |
| setInterval(() => { | |
| if (!simPaused) stepClock() | |
| }, 5000) | |
| // ── Incoming call ────────────────────────────────────────────────────────────── | |
| function simulateCall() { | |
| const a = AGENTS[Math.floor(Math.random() * Math.min(12, AGENTS.length))] | |
| document.getElementById('call-icon').textContent = a.icon | |
| document.getElementById('call-name').textContent = a.label | |
| document.getElementById('call-name').dataset.key = a.key | |
| document.getElementById('call-name').style.color = a.color | |
| document.getElementById('call-role').textContent = a.role | |
| document.getElementById('incoming-call').style.display = 'flex' | |
| logLocal('call.incoming', a.label, 'warn') | |
| } | |
| // ── Feed / NATS (best-effort) ────────────────────────────────────────────────── | |
| const SOURCE_AGENT = { discord: 'herald', telegram: 'nova', council: 'bob', nova: 'bob' } | |
| const SOURCE_ICON = { discord: '📡', telegram: '✈', council: '⬡', nova: '✦' } | |
| const SOURCE_COLOR = { discord: '#5865F2', telegram: '#229ED9', council: '#00ff88', nova: '#bf5af2' } | |
| function logFeedEvent(ev) { | |
| const ts = new Date(ev.ts || Date.now()).toLocaleTimeString('en-US',{hour12:false}) | |
| const icon = SOURCE_ICON[ev.source] || '◌' | |
| const color = SOURCE_COLOR[ev.source] || '#888' | |
| const entry = document.createElement('div') | |
| entry.className = 'log-entry' | |
| entry.innerHTML = `<span class="log-ts">${ts}</span> <span class="log-subject" style="color:${color}">${icon} ${(ev.source||'feed').toUpperCase()}</span> <span class="log-payload">${(ev.payload?.text || ev.type || '').slice(0,100)}</span>` | |
| const body = document.getElementById('logcat-body') | |
| body.insertBefore(entry, body.firstChild) | |
| while (body.children.length > 100) body.removeChild(body.lastChild) | |
| } | |
| function routeToChat(ev) { | |
| const agentKey = SOURCE_AGENT[ev.source] | |
| if (!agentKey || !ev.payload?.text) return | |
| if (!messages[agentKey]) messages[agentKey] = [] | |
| messages[agentKey].push({ role:'agent', text:(SOURCE_ICON[ev.source]||'') + ' ' + ev.payload.text, ts:Date.now() }) | |
| if (screen === 'chat' && activeAgent?.key === agentKey) renderMessages() | |
| buildTree() | |
| } | |
| function pollFeed() { | |
| fetch(`${MESH}/feed?since=${feedSince}`) | |
| .then(r => r.json()) | |
| .then(d => { | |
| if (!d.events?.length) return | |
| d.events.sort((a,b) => a.id - b.id).forEach(ev => { | |
| logFeedEvent(ev) | |
| routeToChat(ev) | |
| if (ev.id > feedSince) feedSince = ev.id | |
| }) | |
| }) | |
| .catch(() => {}) | |
| } | |
| function connectNats() { | |
| try { | |
| const es = new EventSource('https://collectivekitty.com/api/nats/stream?subject=snapkitty.>') | |
| es.addEventListener('message', e => { | |
| try { | |
| const d = JSON.parse(e.data) | |
| natsCount++ | |
| const level = (d.subject||'').includes('error')?'error':(d.subject||'').includes('warn')?'warn':'info' | |
| logLocal(d.subject??'snapkitty.event', JSON.stringify(d.data??d).slice(0,120), level) | |
| document.getElementById('nats-dot').style.color = '#00ff88' | |
| document.getElementById('nats-status').textContent = `● ${natsCount} events` | |
| } catch {} | |
| }) | |
| es.onerror = () => { | |
| document.getElementById('nats-dot').style.color = '#ff453a' | |
| } | |
| } catch { | |
| document.getElementById('nats-dot').style.color = '#ff453a' | |
| } | |
| } | |
| // ── Menus & actions ──────────────────────────────────────────────────────────── | |
| function closeMenus() { | |
| document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('open')) | |
| } | |
| async function runAction(action) { | |
| closeMenus() | |
| switch (action) { | |
| case 'step': stepClock(); break | |
| case 'toggle-pause': togglePause(); break | |
| case 'verify': { | |
| const v = await wormVerify() | |
| logLocal('worm.verify', v.msg, v.ok ? 'info' : 'error') | |
| alert(v.msg) | |
| break | |
| } | |
| case 'worm-dump': { | |
| await proposeAndExecute(activeAgent, 'dump_world', {}, async () => { | |
| await wormAppend('world_dump', activeAgent.key, worldSnapshot()) | |
| return 'dumped' | |
| }) | |
| openWorm() | |
| break | |
| } | |
| case 'clear-ledger': | |
| if (confirm('Clear WORM ledger (testnet only)?')) await clearLedger() | |
| break | |
| case 'export-json': { | |
| const blob = new Blob([JSON.stringify({ world: worldSnapshot(), worm: wormChain }, null, 2)], { type:'application/json' }) | |
| const a = document.createElement('a') | |
| a.href = URL.createObjectURL(blob) | |
| a.download = `sovereign-session-${Date.now()}.json` | |
| a.click() | |
| break | |
| } | |
| case 'open-home': goHome(); break | |
| case 'open-worm': openWorm(); break | |
| case 'open-const': openConstitution(); break | |
| case 'open-term': openTerm(); break | |
| case 'open-browser': openBrowser(); break | |
| case 'open-vm': openVM(); break | |
| case 'focus-term': openTerm(); break | |
| case 'focus-browser': openBrowser(); break | |
| case 'incoming-call': simulateCall(); break | |
| case 'agent-tick': agentTick(); break | |
| case 'show-help': | |
| document.getElementById('help-modal').classList.add('open') | |
| break | |
| } | |
| } | |
| // ── Event wiring ─────────────────────────────────────────────────────────────── | |
| document.querySelectorAll('.menu-item > .menu-btn').forEach(btn => { | |
| btn.addEventListener('click', e => { | |
| e.stopPropagation() | |
| const item = btn.parentElement | |
| const was = item.classList.contains('open') | |
| closeMenus() | |
| if (!was) item.classList.add('open') | |
| }) | |
| }) | |
| document.querySelectorAll('.menu-dropdown button[data-action]').forEach(btn => { | |
| btn.addEventListener('click', () => runAction(btn.dataset.action)) | |
| }) | |
| document.addEventListener('click', () => closeMenus()) | |
| document.querySelectorAll('[data-go-home]').forEach(el => el.addEventListener('click', goHome)) | |
| document.getElementById('chat-back').addEventListener('click', goHome) | |
| document.getElementById('nav-back').addEventListener('click', goHome) | |
| document.getElementById('nav-home-btn').addEventListener('click', goHome) | |
| document.getElementById('nav-recents-chat')?.addEventListener('click', () => openWorm()) | |
| document.querySelectorAll('[data-nav]').forEach(el => { | |
| el.addEventListener('click', () => { | |
| const n = el.dataset.nav | |
| if (n === 'home' || n === 'back') goHome() | |
| if (n === 'recents') openWorm() | |
| }) | |
| }) | |
| document.querySelectorAll('.dock-icon').forEach(el => { | |
| el.addEventListener('click', () => { | |
| const d = el.dataset.dock | |
| if (d === 'phone') simulateCall() | |
| if (d === 'messages') openChat(activeAgent) | |
| if (d === 'browser') openBrowser() | |
| if (d === 'term') openTerm() | |
| }) | |
| }) | |
| document.getElementById('send-btn').addEventListener('click', sendMessage) | |
| document.getElementById('msg-input').addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() } | |
| }) | |
| document.getElementById('msg-input').addEventListener('input', updateSendBtn) | |
| document.getElementById('answer-btn').addEventListener('click', () => { | |
| const callName = document.getElementById('call-name').dataset.key | |
| const a = AGENTS.find(x=>x.key===callName) | |
| if (a) openChat(a) | |
| document.getElementById('incoming-call').style.display = 'none' | |
| }) | |
| document.getElementById('decline-btn').addEventListener('click', () => { | |
| document.getElementById('incoming-call').style.display = 'none' | |
| }) | |
| document.getElementById('browser-go').addEventListener('click', () => navigateBrowser(document.getElementById('browser-url').value)) | |
| document.getElementById('browser-url').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') navigateBrowser(document.getElementById('browser-url').value) | |
| }) | |
| document.getElementById('browser-back').addEventListener('click', () => { | |
| if (browserIndex > 0) { | |
| browserIndex-- | |
| navigateBrowser(browserHistory[browserIndex], false) | |
| } | |
| }) | |
| document.getElementById('browser-fwd').addEventListener('click', () => { | |
| if (browserIndex < browserHistory.length - 1) { | |
| browserIndex++ | |
| navigateBrowser(browserHistory[browserIndex], false) | |
| } | |
| }) | |
| document.getElementById('browser-reload').addEventListener('click', () => { | |
| if (browserIndex >= 0) navigateBrowser(browserHistory[browserIndex], false) | |
| }) | |
| document.getElementById('term-input').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') { e.preventDefault(); onTermSubmit() } | |
| if (e.key === 'ArrowUp') { | |
| e.preventDefault() | |
| if (termHistIdx > 0) { | |
| termHistIdx-- | |
| document.getElementById('term-input').value = termHistory[termHistIdx] || '' | |
| } | |
| } | |
| if (e.key === 'ArrowDown') { | |
| e.preventDefault() | |
| if (termHistIdx < termHistory.length - 1) { | |
| termHistIdx++ | |
| document.getElementById('term-input').value = termHistory[termHistIdx] || '' | |
| } else { | |
| termHistIdx = termHistory.length | |
| document.getElementById('term-input').value = '' | |
| } | |
| } | |
| }) | |
| document.getElementById('worm-verify-btn').addEventListener('click', async () => { | |
| const v = await wormVerify() | |
| logLocal('worm.verify', v.msg, v.ok ? 'info' : 'error') | |
| alert(v.msg) | |
| }) | |
| document.getElementById('close-help').addEventListener('click', () => { | |
| document.getElementById('help-modal').classList.remove('open') | |
| }) | |
| document.getElementById('help-modal').addEventListener('click', e => { | |
| if (e.target.id === 'help-modal') e.target.classList.remove('open') | |
| }) | |
| // Keyboard shortcuts (skip when typing) | |
| document.addEventListener('keydown', e => { | |
| const tag = (e.target.tagName || '').toLowerCase() | |
| if (tag === 'input' || tag === 'textarea' || tag === 'select') return | |
| if (e.key === ' ' || e.code === 'Space') { e.preventDefault(); stepClock() } | |
| else if (e.key === 'p' || e.key === 'P') togglePause() | |
| else if (e.key === 'c' || e.key === 'C') { | |
| if (confirm('Clear WORM ledger (testnet)?')) clearLedger() | |
| } | |
| else if (e.key === 'v' || e.key === 'V') runAction('verify') | |
| else if (e.key === 'w' || e.key === 'W') runAction('worm-dump') | |
| else if (e.key === '?') openConstitution() | |
| else if (e.key === 'b' || e.key === 'B') openBrowser() | |
| else if (e.key === 't' || e.key === 'T') openTerm() | |
| else if (e.key === 'h' || e.key === 'H') goHome() | |
| }) | |
| // ── Init ─────────────────────────────────────────────────────────────────────── | |
| async function init() { | |
| try { | |
| buildTree() | |
| buildHome() | |
| buildInspector() | |
| buildTermAgentSelect() | |
| showScreen('home') | |
| updateSimBadge() | |
| await wormAppend('genesis', null, { note: 'Sovereign Emulator boot', version: '∞.1' }) | |
| termPrint('sys', 'SnapKitty sovereign bash — type help') | |
| termPrint('sys', 'Agents propose · constitution gates · WORM seals') | |
| logLocal('boot', 'Sovereign Emulator v∞.1 — browser + bash online', 'info') | |
| connectNats() | |
| pollFeed() | |
| setInterval(pollFeed, 10000) | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('sw.js').catch(() => {}) | |
| } | |
| // Production hardening: Load persisted state | |
| loadPersistedState() | |
| } catch (e) { | |
| console.error('Initialization failed:', e) | |
| logLocal('boot.error', `Init failed: ${e.message || String(e)}`, 'error') | |
| alert(`Initialization Error: ${e.message || 'Unknown error'}\nCheck console for details.`) | |
| } | |
| } | |
| // Production hardening: State persistence | |
| function loadPersistedState() { | |
| try { | |
| const saved = localStorage.getItem('sovereignEmulatorState') | |
| if (saved) { | |
| const state = JSON.parse(saved) | |
| if (state.wormChain) wormChain = state.wormChain | |
| if (state.messages) messages = state.messages | |
| if (state.councilMembers) councilMembers = state.councilMembers | |
| if (state.simTick !== undefined) simTick = state.simTick | |
| if (state.simPaused !== undefined) simPaused = state.simPaused | |
| logLocal('persistence', 'Loaded persisted state', 'info') | |
| } | |
| } catch (e) { | |
| logLocal('persistence.error', `Failed to load state: ${e.message}`, 'warn') | |
| } | |
| // Auto-save on changes | |
| setInterval(() => { | |
| savePersistedState() | |
| }, 30000) | |
| } | |
| function savePersistedState() { | |
| try { | |
| const state = { | |
| wormChain, | |
| messages, | |
| councilMembers, | |
| simTick, | |
| simPaused, | |
| ts: Date.now() | |
| } | |
| localStorage.setItem('sovereignEmulatorState', JSON.stringify(state)) | |
| } catch (e) { | |
| logLocal('persistence.error', `Failed to save state: ${e.message}`, 'warn') | |
| } | |
| } | |
| // Global error handler | |
| window.addEventListener('error', (e) => { | |
| logLocal('global.error', `Uncaught: ${e.message} at ${e.filename}:${e.lineno}:${e.colno}`, 'error') | |
| }) | |
| window.addEventListener('unhandledrejection', (e) => { | |
| logLocal('promise.error', `Unhandled rejection: ${e.reason}`, 'error') | |
| }) | |
| init() | |
| </script> | |
| </body> | |
| </html> | |
| <script src="https://sovereign-mesh.snapkittywest.workers.dev/nav.js"></script> | |