bastion 0.60.0
This commit is contained in:
@@ -163,9 +163,15 @@
|
||||
{{else if .Sanctioned}}
|
||||
<p class="obs-foot">Your host vouched for this one. There is nothing to unmark
|
||||
here — the tag it carries was issued by your host and is withdrawn there.</p>
|
||||
{{else if and .ID (not $.OwnerCode)}}
|
||||
<p class="obs-foot">Marking a program as yours takes your code: <a href="/setup">set it up</a> first.</p>
|
||||
{{else if .ID}}
|
||||
<form hx-post="/agents/mark" hx-target="#agents" hx-swap="innerHTML">
|
||||
<form hx-post="/agents/mark" hx-target="#agents" hx-swap="innerHTML" autocomplete="off">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
{{/* hx-preserve: this list is swapped every 10s, and a half-typed code
|
||||
must survive it. */}}
|
||||
<input class="code" id="mark-code-{{.ID}}" hx-preserve="true" name="code" inputmode="numeric" pattern="[0-9]*"
|
||||
autocomplete="one-time-code" placeholder="6-digit code" aria-label="Your code">
|
||||
<button class="go" type="submit">This one is mine</button>
|
||||
</form>
|
||||
{{else}}
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Privatae — approvals</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
{{if not .TrayView}}<link rel="stylesheet" href="/assets/home.css">{{end}}
|
||||
<!-- Same CSP reasoning as page.html: htmx must not inject its indicator styles. -->
|
||||
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
||||
<script src="/assets/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Approvals</h1>
|
||||
<p class="sub">What your hosts are waiting on you for.{{if not .TrayView}} <a href="/">This machine</a>{{end}}</p>
|
||||
<h1>Waiting for you</h1>
|
||||
<p class="sub">What is waiting on your answer: asked of this machine, or by a Host.</p>
|
||||
{{if not .TrayView}}{{template "nav" "approvals"}}{{end}}
|
||||
</header>
|
||||
|
||||
<!-- NOT polled, unlike the front page. The list is redrawn only when the person
|
||||
@@ -54,11 +56,16 @@
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button class="no" type="submit">Deny</button>
|
||||
</form>
|
||||
{{if $.Limited}}
|
||||
<p class="approval-line">Approving signs with this device's key, which is the administrator's:
|
||||
<code>sudo -H privatae-bastion ui</code></p>
|
||||
{{else}}
|
||||
<form hx-post="/device/approve" hx-target="#approvals" hx-swap="innerHTML">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
{{if .NeedsCode}}<input name="code" inputmode="numeric" autocomplete="one-time-code" placeholder="your code" required>{{end}}
|
||||
<button class="go" type="submit">Approve</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Plan}}
|
||||
<p class="approval-foot">Asked {{.At}}. Approving signs this ship plan with this device's key:
|
||||
@@ -70,7 +77,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if gt .NoCodeApprovals 1}}
|
||||
{{if and (gt .NoCodeApprovals 1) (not .Limited)}}
|
||||
{{/* "All" means all SHOWN: the form carries the ids drawn above and nothing
|
||||
else, so an ask that lands after this was drawn is left waiting. A plan
|
||||
that runs root work is never in it: it needs the code, one at a time. */}}
|
||||
@@ -81,17 +88,40 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .MachineAsks}}
|
||||
<h3 class="urgent">Asked of this machine</h3>
|
||||
{{range .MachineAsks}}
|
||||
<div class="approval">
|
||||
<div class="approval-head">Request <span class="id">{{.Short}}</span></div>
|
||||
<p class="approval-line">{{.Text}}</p>
|
||||
<div class="approval-acts">
|
||||
<form hx-post="/approvals/machine/deny" hx-target="#approvals" hx-swap="innerHTML">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button class="no" type="submit">Deny</button>
|
||||
</form>
|
||||
<form hx-post="/approvals/machine/approve" hx-target="#approvals" hx-swap="innerHTML" autocomplete="off">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"
|
||||
placeholder="your code" aria-label="Your code" required>
|
||||
<button class="go" type="submit">Approve</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="approval-foot">Asked {{.At}}. Approving takes your code; anything that can drive your browser can click Approve.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Approvals}}
|
||||
<h3 class="urgent">Waiting for your code on this machine</h3>
|
||||
{{template "stepup-approvals" .}}
|
||||
{{end}}
|
||||
|
||||
{{if and (not .DeviceApprovals) (not .Approvals)}}
|
||||
{{if and (not .DeviceApprovals) (not .Approvals) (not .MachineAsks)}}
|
||||
<p class="approval-line">Nothing is waiting for your approval.</p>
|
||||
{{if .TrayView}}
|
||||
<p class="approval-foot">When your Host asks for something on this machine, it shows here and
|
||||
this window asks for a code from the authenticator app enrolled when this machine's bastion was set up.</p>
|
||||
{{else if not .IsDevice}}
|
||||
{{else if and (not .IsDevice) .Pairings}}
|
||||
<p class="approval-foot">This machine is not your approval device, so asks from your
|
||||
hosts are not answered here. Run <code>privatae-bastion device-init</code> on the machine
|
||||
you want to approve from.</p>
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
<input type="hidden" name="family" value="{{.Name}}">
|
||||
{{if .SwitchedOff}}
|
||||
<input type="hidden" name="off" value="0">
|
||||
{{/* hx-preserve: this page is swapped every 10s and a half-typed code
|
||||
must survive it. Turning back on takes the owner's code. */}}
|
||||
<input class="code" id="on-code-{{$h.Host}}-{{.Name}}" hx-preserve="true" name="code" inputmode="numeric"
|
||||
pattern="[0-9]*" autocomplete="one-time-code" placeholder="your code" aria-label="Your code">
|
||||
<button class="go" type="submit">Turn back on</button>
|
||||
{{else}}
|
||||
<input type="hidden" name="off" value="1">
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* The agents home page and an agent's page (uihome.go). */
|
||||
|
||||
.topnav { display: flex; gap: 1.1rem; margin: 0.9rem 0 0; font-size: 0.9rem; flex-wrap: wrap; }
|
||||
.topnav a { color: var(--dim); text-decoration: none; padding-bottom: 0.2rem; border-bottom: 2px solid transparent; }
|
||||
.topnav a:hover { color: var(--ink); }
|
||||
.topnav a.on { color: var(--ink); border-bottom-color: var(--ok); }
|
||||
|
||||
.tally { margin: 0 0 1rem; color: var(--dim); }
|
||||
h2.family { font-size: 0.95rem; font-weight: 600; margin: 1.6rem 0 0.6rem; }
|
||||
|
||||
.agent-row {
|
||||
display: block; color: var(--ink); text-decoration: none;
|
||||
background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--line);
|
||||
border-radius: 6px; padding: 0.75rem 1rem; margin-bottom: 0.6rem;
|
||||
}
|
||||
a.agent-row:hover { border-color: #3d4452; }
|
||||
.agent-row.waiting { border-left-color: var(--warn); }
|
||||
.agent-row.service { opacity: 0.75; }
|
||||
.agent-head { font-weight: 600; overflow-wrap: anywhere; }
|
||||
.agent-line { margin: 0.15rem 0 0; font-size: 0.88rem; overflow-wrap: anywhere; }
|
||||
.agent-line.urgent { color: var(--warn); }
|
||||
.dim { color: var(--dim); }
|
||||
.agent-line code, .panel code { font-size: 0.8rem; }
|
||||
|
||||
.flags { margin-top: 0.45rem; }
|
||||
.flag {
|
||||
display: inline-block; font-size: 0.75rem; padding: 0.05rem 0.5rem; margin-right: 0.35rem;
|
||||
border-radius: 999px; border: 1px solid var(--line); color: var(--dim);
|
||||
}
|
||||
.flag.waiting, .flag.warn { color: var(--warn); border-color: #5a4524; }
|
||||
.flag.bad { color: var(--bad); border-color: #5a2a27; }
|
||||
.flag.ok { color: var(--ok); border-color: #2a4a33; }
|
||||
|
||||
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 0.75rem 1rem; }
|
||||
.panel p { margin: 0.25rem 0; }
|
||||
.tool { padding: 0.35rem 0; border-bottom: 1px solid var(--line); }
|
||||
.tool:last-child { border-bottom: 0; }
|
||||
.problem { color: var(--warn); font-size: 0.88rem; }
|
||||
.reason { font-size: 0.85rem; }
|
||||
.facts dl { display: grid; grid-template-columns: 7rem 1fr; gap: 0.2rem 0.8rem; margin: 0 0 0.5rem; }
|
||||
.facts dt { color: var(--dim); }
|
||||
.facts dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.panel a, footer a { color: var(--ink); }
|
||||
.acts { margin: 0 0 1rem; }
|
||||
.acts form, .panel form { display: flex; gap: 0.5rem; align-items: center; margin: 0.4rem 0; }
|
||||
.filters { display: flex; gap: 0.9rem; flex-wrap: wrap; font-size: 0.85rem; margin: 0 0 1rem; }
|
||||
.filters a { color: var(--dim); text-decoration: none; }
|
||||
.filters a.on { color: var(--ink); text-decoration: underline; }
|
||||
.activity { display: grid; grid-template-columns: 7.5rem 1fr; gap: 0.8rem; padding: 0.45rem 0;
|
||||
border-bottom: 1px solid var(--line); font-size: 0.9rem; }
|
||||
.activity .when { color: var(--dim); white-space: nowrap; }
|
||||
.activity .what { overflow-wrap: anywhere; }
|
||||
.tool-sub { margin: 0.25rem 0 0 0.8rem; font-size: 0.88rem; }
|
||||
.machine-sections { margin-top: 1.5rem; }
|
||||
.inline-form { display: flex; gap: 0.5rem; align-items: center; margin: 0.35rem 0; flex-wrap: wrap; }
|
||||
.start { background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--ok);
|
||||
border-radius: 6px; padding: 0.75rem 1rem; margin-bottom: 1.2rem; }
|
||||
.start h2 { font-size: 1rem; margin: 0 0 0.4rem; }
|
||||
.start ol { margin: 0; padding-left: 1.2rem; }
|
||||
.start li { margin: 0.25rem 0; }
|
||||
.start li.done { color: var(--dim); }
|
||||
.start li.done::marker { content: "✓ "; color: var(--ok); }
|
||||
.start li.optional { color: var(--dim); }
|
||||
.start a { color: var(--ink); }
|
||||
@@ -0,0 +1,333 @@
|
||||
{{/* The console's sections (plan Addendum 4). One list, used by every page
|
||||
that has a header, so a section is added in one place. The argument is
|
||||
the section the page belongs to. */}}
|
||||
{{define "nav"}}<nav class="topnav">
|
||||
<a href="/"{{if eq . "agents"}} class="on"{{end}}>Agents</a>
|
||||
<a href="/approvals"{{if eq . "approvals"}} class="on"{{end}}>Waiting for you</a>
|
||||
<a href="/activity"{{if eq . "activity"}} class="on"{{end}}>Activity</a>
|
||||
<a href="/machine"{{if eq . "machine"}} class="on"{{end}}>This machine</a>
|
||||
<a href="/setup"{{if eq . "settings"}} class="on"{{end}}>Settings</a>
|
||||
</nav>{{end}}
|
||||
|
||||
{{define "home-page"}}<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Privatae bastion — agents on this machine</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/home.css">
|
||||
<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
|
||||
<script src="/assets/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Agents on this machine</h1>
|
||||
<p class="sub">Every AI agent running here, and what each one is doing.</p>
|
||||
{{template "nav" "agents"}}
|
||||
</header>
|
||||
{{/* Ten seconds: building this walks every process, reads each session's
|
||||
record and the tail of its transcript. Nothing here changes faster. */}}
|
||||
<main id="home" hx-get="/fragments/home" hx-trigger="every 10s" hx-swap="innerHTML">
|
||||
{{template "home" .}}
|
||||
</main>
|
||||
<footer>
|
||||
The detail behind each flag:
|
||||
<a href="/policy">the stopping rule</a> ·
|
||||
<a href="/agents">programs marked as yours</a> ·
|
||||
<a href="/given">what this machine has given away</a>.<br>
|
||||
privatae-bastion {{.Version}} · served from this machine only and not reachable from anywhere else.
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "home"}}
|
||||
{{if .Blind}}
|
||||
<div class="state bad">
|
||||
<h2>This machine cannot watch itself.</h2>
|
||||
<p>This build has no way to list processes on this platform, so an empty list means nothing looked, not
|
||||
that nothing is running.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .WalkError}}
|
||||
<div class="state bad">
|
||||
<h2>The list of agents could not be read.</h2>
|
||||
<p>{{.WalkError}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .PerAccount}}
|
||||
<div class="state warn">
|
||||
<h2>This bastion runs in your own account.</h2>
|
||||
<p>An AI agent running as you can stop it, and can read the secret behind your code. Install it for the
|
||||
whole machine to keep agents out.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if or (not .CodeSet) (not .BaselineSet)}}
|
||||
<div class="start">
|
||||
<h2>Getting started</h2>
|
||||
<ol>
|
||||
<li class="done">See what is running on this machine. That is this page, with no setup.</li>
|
||||
<li{{if .CodeSet}} class="done"{{end}}>{{if .CodeSet}}Your code is set up.{{else}}<a href="/setup">Set up your code</a>, so you can stop an
|
||||
agent and answer what agents ask. It takes an authenticator app on your phone.{{end}}</li>
|
||||
<li{{if .BaselineSet}} class="done"{{end}}>{{if .BaselineSet}}A baseline is recorded.{{else}}<a href="/machine">Record a baseline</a> on
|
||||
This machine, so anything new that starts here stands out.{{end}}</li>
|
||||
<li class="optional">{{if .HostsPaired}}A Host is connected.{{else}}Optional: <a href="/setup">connect a Host</a>, if you
|
||||
run one. Everything above works without it.{{end}}</li>
|
||||
</ol>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Waiting}}
|
||||
<div class="approval">
|
||||
<div class="approval-head">{{.Waiting}} request{{if gt .Waiting 1}}s are{{else}} is{{end}} waiting for your approval</div>
|
||||
<p class="approval-line"><a href="/approvals">Open Waiting for you</a> to read and answer {{if gt .Waiting 1}}them{{else}}it{{end}}.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .WalkError}}
|
||||
<p class="tally">
|
||||
{{if or .Sessions .Others}}{{if .Sessions}}{{.Sessions}} session{{if gt .Sessions 1}}s{{end}}{{end}}{{if and .Sessions .Others}} · {{end}}{{if .Others}}{{.Others}} other program{{if gt .Others 1}}s{{end}}{{end}}{{else}}No AI agents are running here right now.{{end}}
|
||||
{{if .WaitingForYou}}<span class="flag waiting">{{.WaitingForYou}} waiting for you</span>{{end}}
|
||||
{{if .NotYours}}<span class="flag">{{.NotYours}} not marked as yours</span>{{end}}
|
||||
{{if .WouldStop}}<span class="flag bad">{{.WouldStop}} the stopping rule would stop</span>{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{range .Families}}
|
||||
<h2 class="family">{{.Heading}}</h2>
|
||||
{{range .Rows}}
|
||||
{{if .Service}}
|
||||
<div class="agent-row service">
|
||||
<div class="agent-head">{{.Head}}</div>
|
||||
<div class="agent-line">{{.State}}</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="agent-row{{if .WaitingForYou}} waiting{{end}}" href="/agent?pid={{.PID}}">
|
||||
<div class="agent-head">{{.Head}}</div>
|
||||
{{if .State}}<div class="agent-line{{if .WaitingForYou}} urgent{{end}}">{{.State}}</div>{{end}}
|
||||
{{if .Where}}<div class="agent-line dim">{{.Where}}</div>{{end}}
|
||||
{{if .RunningNow}}<div class="agent-line"><span class="dim">running now:</span> <code>{{.RunningNow}}</code></div>{{end}}
|
||||
{{if .ToolServers}}<div class="agent-line dim">tool servers: {{.ToolServers}}</div>{{end}}
|
||||
{{if or .PermissionsOff .NotYours .WouldStop}}
|
||||
<div class="flags">
|
||||
{{if .PermissionsOff}}<span class="flag bad">permission prompts off</span>{{end}}
|
||||
{{if .NotYours}}<span class="flag">not marked as yours</span>{{end}}
|
||||
{{if .WouldStop}}<span class="flag bad">the stopping rule would stop it</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<p class="checked-at">Checked {{.CheckedAt}}. Names, titles and statuses are what each agent says about itself.</p>
|
||||
{{end}}
|
||||
|
||||
{{define "agent-page"}}<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Privatae bastion — {{if .Found}}{{.Family}}: {{.Row.Head}}{{else}}agent not running{{end}}</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/home.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "nav" "agents"}}
|
||||
{{if .Found}}
|
||||
<h1>{{.Row.Head}}</h1>
|
||||
<p class="sub">{{.Family}} · pid {{.PID}}</p>
|
||||
{{else}}
|
||||
<h1>Not running</h1>
|
||||
<p class="sub">No agent this bastion can see has pid {{.PID}} now. It may have finished. <a href="/">Back to the agents</a>.</p>
|
||||
{{end}}
|
||||
</header>
|
||||
<main>
|
||||
{{if .Flash}}<div class="flash {{.FlashClass}}">{{.Flash}}</div>{{end}}
|
||||
{{if .Found}}
|
||||
{{if and (not .Row.App) (not .Row.Service)}}
|
||||
<div class="acts">
|
||||
{{if .OwnerCode}}
|
||||
<form method="post" action="/agent/stop" autocomplete="off">
|
||||
<input type="hidden" name="pid" value="{{.PID}}">
|
||||
<input type="hidden" name="signature" value="{{.Signature}}">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"
|
||||
placeholder="your code" aria-label="Your code" required>
|
||||
<button class="no" type="submit">Stop this session</button>
|
||||
</form>
|
||||
<p class="dim">Stops it and every process it started. Your code is needed because anything that can drive your
|
||||
browser can click this button.</p>
|
||||
{{else}}
|
||||
<p class="dim">To stop agents from here, <a href="/setup">set up your code</a> first.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<h3>What it is doing</h3>
|
||||
<div class="panel">
|
||||
{{if .Row.State}}<p class="agent-line{{if .Row.WaitingForYou}} urgent{{end}}">{{.Row.State}}</p>{{end}}
|
||||
{{if .Row.Where}}<p class="agent-line">{{.Row.Where}}</p>{{end}}
|
||||
{{if .Row.RunningNow}}<p class="agent-line"><span class="dim">running now:</span> <code>{{.Row.RunningNow}}</code></p>{{end}}
|
||||
{{if .Self}}{{if .Self.LastPrompt}}<p class="agent-line"><span class="dim">last asked:</span> “{{.Self.LastPrompt}}”</p>{{end}}{{end}}
|
||||
{{if .Row.PermissionsOff}}<p class="agent-line urgent">Permission prompts are off for this session: it acts without asking first.</p>{{end}}
|
||||
{{if .Self}}
|
||||
<p class="note">The name, title, status and last prompt are what {{.Family}} says about this session, read from its
|
||||
own files on this machine{{if not .Self.Verified}}. This platform cannot confirm the record belongs to this exact
|
||||
process{{end}}.</p>
|
||||
{{else}}
|
||||
<p class="note">This agent keeps no session record this bastion can read, so it has no name or title here.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<h3>Its MCP tool servers</h3>
|
||||
<div class="panel">
|
||||
{{if .Tools}}
|
||||
{{range .Tools}}
|
||||
<div class="tool">
|
||||
<div><strong>{{.Name}}</strong> <span class="dim">in {{.Where}}</span></div>
|
||||
{{range .Problems}}<div class="problem">{{.}}</div>{{else}}<div class="dim">no problems found</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="dim">No settings file this bastion can read gives this agent a tool server.</p>
|
||||
{{end}}
|
||||
{{if .Row.ToolServers}}<p class="dim">Running now: {{.Row.ToolServers}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
<h3>Whether it is yours</h3>
|
||||
<div class="panel">
|
||||
<p>{{if .Sanctioned}}<span class="flag ok">yours</span>{{else}}<span class="flag">not marked as yours</span>{{end}} {{.SanctionWhy}}</p>
|
||||
{{if and (not .Sanctioned) .MarkID (not .Row.App) .OwnerCode}}
|
||||
<form method="post" action="/agent/mark" autocomplete="off">
|
||||
<input type="hidden" name="pid" value="{{.PID}}">
|
||||
<input type="hidden" name="id" value="{{.MarkID}}">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*"
|
||||
autocomplete="one-time-code" placeholder="your code" aria-label="Your code" required>
|
||||
<button class="go" type="submit">This one is mine</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if and (not .Sanctioned) (not .OwnerCode)}}<p class="dim">Marking a program as yours takes <a href="/setup">your code</a>; set it up first.</p>{{end}}
|
||||
<p class="dim">Every mark, and taking one back: <a href="/agents">programs marked as yours</a>.</p>
|
||||
</div>
|
||||
|
||||
<h3>The stopping rule</h3>
|
||||
<div class="panel">
|
||||
<p><span class="flag {{.VerdictCls}}">{{.Verdict}}</span> The rule only rehearses: it stops nothing on its own.</p>
|
||||
{{range .Reasons}}<p class="reason"><strong>{{.Term}}</strong> {{.Detail}}</p>{{end}}
|
||||
</div>
|
||||
|
||||
<h3>How the bastion knows</h3>
|
||||
<div class="panel facts">
|
||||
<dl>
|
||||
<dt>Kind</dt><dd>{{.Kind}}</dd>
|
||||
{{if .Exe}}<dt>File</dt><dd><code>{{.Exe}}</code></dd>{{end}}
|
||||
<dt>Command</dt><dd><code>{{.Cmdline}}</code></dd>
|
||||
{{if .Origin}}<dt>Started by</dt><dd>{{.Origin}}</dd>{{end}}
|
||||
{{if .Endpoints}}<dt>Talks to</dt><dd>{{range $i, $e := .Endpoints}}{{if $i}}, {{end}}{{$e}}{{end}}</dd>{{end}}
|
||||
{{if .Self}}{{if .Self.SessionID}}<dt>Session</dt><dd><code>{{.Self.SessionID}}</code></dd>{{end}}{{end}}
|
||||
</dl>
|
||||
{{range .Evidence}}<p class="reason"><span class="dim">{{.Tier}}</span> {{.Signal}}{{if .Value}} · {{.Value}}{{end}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
<footer>privatae-bastion {{.Version}} · served from this machine only and not reachable from anywhere else.</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "activity-page"}}<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Privatae bastion — activity on this machine</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/home.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Activity</h1>
|
||||
<p class="sub">What happened on this machine in the last seven days, newest first.</p>
|
||||
{{template "nav" "activity"}}
|
||||
</header>
|
||||
<main>
|
||||
<p class="filters">
|
||||
<a href="/activity"{{if eq .Kind ""}} class="on"{{end}}>Everything</a>
|
||||
<a href="/activity?kind=stop"{{if eq .Kind "stop"}} class="on"{{end}}>Stops ({{index .Counts "stop"}})</a>
|
||||
<a href="/activity?kind=approval"{{if eq .Kind "approval"}} class="on"{{end}}>Your approvals ({{index .Counts "approval"}})</a>
|
||||
<a href="/activity?kind=host"{{if eq .Kind "host"}} class="on"{{end}}>What Hosts did ({{index .Counts "host"}})</a>
|
||||
<a href="/activity?kind=change"{{if eq .Kind "change"}} class="on"{{end}}>Changes ({{index .Counts "change"}})</a>
|
||||
</p>
|
||||
{{range .Items}}
|
||||
<div class="activity{{if .Refused}} refused{{end}}">
|
||||
<span class="when">{{.At.Local.Format "Jan 2 15:04"}}</span>
|
||||
<span class="what">{{if .Who}}<strong>{{.Who}}</strong> {{end}}{{.What}}{{if .Refused}} <span class="flag bad">refused</span>{{end}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="dim">Nothing has happened here in the last seven days{{if .Kind}} of this kind{{end}}.</p>
|
||||
{{end}}
|
||||
<p class="note">Read from the records this bastion keeps on this machine. What a Host did here is also written to the
|
||||
machine's own system log, where the Host cannot change it.</p>
|
||||
</main>
|
||||
<footer>privatae-bastion {{.Version}} · served from this machine only and not reachable from anywhere else.</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "machine-schedules"}}
|
||||
{{if .Items}}
|
||||
{{range .Items}}
|
||||
<div class="tool">
|
||||
<div><strong>{{.Agent}}</strong> <span class="dim">{{if .Schedule}}runs {{.Schedule}} · {{end}}{{.Where}}{{if .User}} · as {{.User}}{{end}}</span></div>
|
||||
<div><code>{{.Command}}</code></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else if .Supported}}
|
||||
<p class="dim">No scheduled job on this machine starts an AI agent.</p>
|
||||
{{else}}
|
||||
<p class="dim">This platform's schedulers cannot be read by this build, so an empty list here means nothing looked.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "machine-tools"}}
|
||||
{{range .}}
|
||||
<div class="tool">
|
||||
<div><strong>{{.Client}}</strong> <span class="dim">{{len .Servers}} tool server{{if gt (len .Servers) 1}}s{{end}}{{if .Bad}}, {{.Bad}} with a problem{{end}}</span></div>
|
||||
{{range .Servers}}{{if .Problems}}
|
||||
<div class="tool-sub"><span>{{.Name}}</span>{{range .Problems}}<div class="problem">{{.}}</div>{{end}}</div>
|
||||
{{end}}{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="dim">No MCP tool server is configured in any settings file this bastion can read.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "machine-baseline"}}
|
||||
{{if .Flash}}<div class="flash {{.FlashClass}}">{{.Flash}}</div>{{end}}
|
||||
{{if .Has}}
|
||||
<p>Baseline recorded {{.TakenAt}}, {{.Entries}} entries.{{if .Note}} <span class="flag bad">{{.Note}}</span>{{end}}</p>
|
||||
<form hx-post="/machine/compare" hx-target="#machine-baseline" hx-swap="innerHTML">
|
||||
<button class="go" type="submit">Compare what runs now</button>
|
||||
</form>
|
||||
{{if .Compared}}
|
||||
<p class="dim">Compared at {{.ComparedAt}}.{{if .WalkFailed}} The process walk failed ({{.WalkFailed}}): nothing was compared.{{end}}</p>
|
||||
{{range .Findings}}<div class="problem">{{.}}</div>{{else}}{{if not .WalkFailed}}<p>Nothing running here is outside the baseline.</p>{{end}}{{end}}
|
||||
<p class="dim">Something started in the last few seconds is judged on the next comparison, so a short command is not reported as new.</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<p class="dim">No baseline yet. A baseline records what normally runs here, so anything new stands out.</p>
|
||||
{{end}}
|
||||
{{if .OwnerCode}}
|
||||
<form hx-post="/machine/baseline" hx-target="#machine-baseline" hx-swap="innerHTML" autocomplete="off">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"
|
||||
placeholder="your code" aria-label="Your code" required>
|
||||
<button type="submit">{{if .Has}}Record a new baseline{{else}}Record the baseline{{end}}</button>
|
||||
</form>
|
||||
<p class="dim">Recording makes everything running now count as normal, so it takes your code.</p>
|
||||
{{else}}
|
||||
<p class="dim">To record a baseline, <a href="/setup">set up your code</a> first.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Privatae bastion — this machine</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/given.css">
|
||||
<link rel="stylesheet" href="/assets/home.css">
|
||||
<!-- htmx injects its own indicator stylesheet with insertAdjacentHTML (it does
|
||||
`<style>` + `.htmx-indicator{opacity:0...}` when includeIndicatorStyles is
|
||||
true, which is its default). This page's CSP is `default-src 'self'` with no
|
||||
@@ -25,26 +26,13 @@
|
||||
<body>
|
||||
<header>
|
||||
<h1>This machine</h1>
|
||||
<p class="sub">The bastion running here, and what it is doing.</p>
|
||||
<!-- The links off this page, all in the header rather than the footer on
|
||||
purpose. None of these is a footnote: which host may read this screen,
|
||||
read these files or open a root shell here; the way in for someone who
|
||||
has not set the machine up yet; what is running here that nobody has
|
||||
vouched for; and what a rule would stop if it were allowed to. They sit
|
||||
outside <main>, so the 3s htmx swap never touches them.
|
||||
|
||||
Five units each added one link without knowing about the others, and
|
||||
each merge duplicated the block. Resolved by hand
|
||||
at integration; the order below is deliberate — set it up, then what you
|
||||
gave away, then what is on the disk, then what is running, then what a
|
||||
rule would do about it. -->
|
||||
<p class="sub">The bastion running here, and the Hosts it serves.</p>
|
||||
{{template "nav" "machine"}}
|
||||
<!-- The older pages, until the steps that replace them land (plan Addendum 4):
|
||||
what each Host may do here, and the disk. -->
|
||||
<p class="sub">
|
||||
<a href="/approvals">Approvals</a> ·
|
||||
<a href="/setup">Set this machine up</a> ·
|
||||
<a href="/given">What this machine has given away</a> ·
|
||||
<a href="/diskmap">This machine's disk</a> ·
|
||||
<a href="/agents">Automated programs running here</a> ·
|
||||
<a href="/policy">What would be stopped</a>
|
||||
<a href="/diskmap">This machine's disk</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -58,6 +46,23 @@
|
||||
{{template "status" .}}
|
||||
</main>
|
||||
|
||||
{{/* Loaded once when the page opens (uimachine.go): they read files and walk
|
||||
processes, and nothing in them changes at the status block's pace. */}}
|
||||
<main class="machine-sections">
|
||||
<h3>Report</h3>
|
||||
<div class="panel">
|
||||
<form method="post" action="/machine/report"><button class="go" type="submit">Write report</button></form>
|
||||
<p class="dim">One Markdown file: the agents running now, scheduled jobs that start one, MCP tool-server problems,
|
||||
changes since your baseline, and the last seven days of activity. No AI model writes any of it.</p>
|
||||
</div>
|
||||
<h3>Scheduled jobs that start an agent</h3>
|
||||
<div class="panel" hx-get="/fragments/machine/schedules" hx-trigger="load" hx-swap="innerHTML"><p class="dim">Reading…</p></div>
|
||||
<h3>MCP tool servers on this machine</h3>
|
||||
<div class="panel" hx-get="/fragments/machine/tools" hx-trigger="load" hx-swap="innerHTML"><p class="dim">Reading…</p></div>
|
||||
<h3>Changes since your baseline</h3>
|
||||
<div class="panel" id="machine-baseline" hx-get="/fragments/machine/baseline" hx-trigger="load" hx-swap="innerHTML"><p class="dim">Reading…</p></div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
privatae-bastion {{.Version}} · this page is served from this machine only and
|
||||
is not reachable from anywhere else.
|
||||
|
||||
+123
-33
@@ -7,6 +7,7 @@
|
||||
<title>Privatae bastion — set this machine up</title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/setup.css">
|
||||
<link rel="stylesheet" href="/assets/home.css">
|
||||
<!-- htmx's own indicator stylesheet is injected with insertAdjacentHTML and is
|
||||
blocked by this page's CSP (default-src 'self', no style-src escape, no
|
||||
nonce). Turn the injection off rather than widen the policy — see
|
||||
@@ -16,9 +17,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Set this machine up</h1>
|
||||
<p class="sub">Pairing, a second factor, folders, the anchor and the owner name.
|
||||
<a href="/">Back to what this machine is doing</a>.</p>
|
||||
<h1>Settings</h1>
|
||||
<p class="sub">Your code, connecting a Host, folders, the Host in charge and the owner name.</p>
|
||||
{{template "nav" "settings"}}
|
||||
</header>
|
||||
|
||||
<main id="setup">
|
||||
@@ -45,35 +46,107 @@
|
||||
it — an anchor on a local box means nothing, and a folder needs a host. */}}
|
||||
<div class="host">
|
||||
<dl class="host-facts">
|
||||
<dt>Mode</dt><dd>{{.Mode}}</dd>
|
||||
<dt>Anchor</dt>
|
||||
<dt>This computer</dt>
|
||||
<dd>{{if .Networked}}A shared machine: several Hosts may be connected, and the Host in charge approves each new one.
|
||||
{{else}}Your own computer: one Host at most, chosen here.{{end}}</dd>
|
||||
<dt>Host in charge</dt>
|
||||
{{if .AnchorHost}}<dd>{{.AnchorHost}}{{if .AnchorOwner}} (owner “{{.AnchorOwner}}”){{end}}</dd>
|
||||
{{else}}<dd>none{{if .AnchorRequired}} <span class="tag bad">required</span>{{end}}</dd>{{end}}
|
||||
<dt>Owner</dt><dd>{{if .Owner}}{{.Owner}}{{else}}not claimed{{end}}</dd>
|
||||
{{else if .AnchorRequired}}<dd>none <span class="tag bad">required</span> Until one is chosen, this machine serves no Host.</dd>
|
||||
{{else}}<dd>not needed on your own computer</dd>{{end}}
|
||||
<dt>Owner name</dt><dd>{{if .Owner}}{{.Owner}}{{else}}not set{{end}}</dd>
|
||||
<dt>Installed</dt><dd>{{if .Whole}}for the whole machine{{else}}in your own account <span class="tag warn">agents running as you can stop it</span>{{end}}</dd>
|
||||
</dl>
|
||||
{{if .Hosts}}
|
||||
<dl class="host-facts setup-hosts">
|
||||
{{range .Hosts}}
|
||||
<dt>{{.Name}}</dt>
|
||||
<dd>
|
||||
{{.State}}
|
||||
{{if .IsAnchor}}<span class="tag ok">anchor</span>{{end}}
|
||||
{{if .Active}}
|
||||
{{if .Serving}}<span class="tag ok">serving</span>
|
||||
{{else}}<span class="tag bad">not answering</span>{{end}}
|
||||
{{end}}
|
||||
{{if .Stepup}}<span class="tag ok">second factor</span>
|
||||
{{else}}<span class="tag warn">no second factor</span>{{end}}
|
||||
</dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* ── 1. pair ───────────────────────────────────────────────────────────── */}}
|
||||
<h3>Pair a host</h3>
|
||||
{{/* ── 0. the owner's own code (stepup_machine.go) ─────────────────────── */}}
|
||||
<h3>Your code</h3>
|
||||
<div class="host">
|
||||
<p class="sub">A six-digit code from an authenticator app on your phone. Stopping an agent, marking one as
|
||||
yours, and answering what is asked of this machine take it. An AI agent can click a button on this page
|
||||
if it can drive your browser; it cannot read your phone.</p>
|
||||
{{if and .Enrol .Enrol.Machine}}
|
||||
<div class="qr-wrap">
|
||||
<img class="qr" src="/setup/stepup/qr?e={{.Enrol.Handle}}" width="320" height="320"
|
||||
alt="QR code for your code on this machine">
|
||||
<div class="qr-side">
|
||||
<p>Point your authenticator app at the square. It will appear as <strong>Privatae bastion</strong>.</p>
|
||||
<form hx-post="/setup/stepup/confirm" hx-target="#setup" hx-swap="innerHTML" autocomplete="off">
|
||||
<input type="hidden" name="enrol" value="{{.Enrol.Handle}}">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*"
|
||||
autocomplete="one-time-code" placeholder="6-digit code"
|
||||
aria-label="A code from the app you just scanned with">
|
||||
<button class="go" type="submit">Confirm</button>
|
||||
</form>
|
||||
<p class="sub">Nothing is written to this machine until that code matches.</p>
|
||||
<form hx-post="/setup/stepup/key" hx-target="#stepup-key" hx-swap="innerHTML">
|
||||
<input type="hidden" name="enrol" value="{{.Enrol.Handle}}">
|
||||
<button type="submit">Cannot scan? Show the key</button>
|
||||
</form>
|
||||
<div id="stepup-key"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if .OwnerCode}}
|
||||
<p><span class="flag ok">set up</span> Your code is set up on this machine.</p>
|
||||
{{if .Limited}}
|
||||
<p class="sub">To replace it, at a terminal: <code>sudo -H privatae-bastion stepup-enroll --machine --rotate</code></p>
|
||||
{{else}}
|
||||
<form class="setup-form" hx-post="/setup/stepup/start" hx-target="#setup" hx-swap="innerHTML">
|
||||
<input type="hidden" name="target" value="machine">
|
||||
<label class="tick"><input type="checkbox" name="replace" value="1"> Replace it (the app set up now stops working)</label>
|
||||
<button type="submit">Set up a new code</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{else if .Limited}}
|
||||
<p class="sub">Set it up once, at a terminal, as the machine's administrator, so no program running as you
|
||||
can set it up first: <code>sudo -H privatae-bastion stepup-enroll --machine</code></p>
|
||||
{{else}}
|
||||
<form class="setup-form" hx-post="/setup/stepup/start" hx-target="#setup" hx-swap="innerHTML">
|
||||
<input type="hidden" name="target" value="machine">
|
||||
<button class="go" type="submit">Set up your code</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<h3>Connected Hosts</h3>
|
||||
<div class="host">
|
||||
{{range .Hosts}}
|
||||
<div class="tool">
|
||||
<div><strong>{{.Name}}</strong> <span class="dim">{{.State}}</span>
|
||||
{{if .IsAnchor}}<span class="tag ok">in charge</span>{{end}}
|
||||
{{if .Active}}{{if .Serving}}<span class="tag ok">connected</span>{{else}}<span class="tag bad">not answering</span>{{end}}{{end}}
|
||||
{{if .QuarantinedUntil}}<span class="tag bad">quarantined until {{.QuarantinedUntil}}</span>{{end}}
|
||||
</div>
|
||||
<p class="sub"><a href="/given">What it may do here</a></p>
|
||||
{{if $.Whole}}
|
||||
{{if .QuarantinedUntil}}
|
||||
<form class="inline-form" hx-post="/setup/host/release" hx-target="#setup" hx-swap="innerHTML" autocomplete="off">
|
||||
<input type="hidden" name="host" value="{{.Name}}">
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="your code" required>
|
||||
<button class="go" type="submit">Release from quarantine</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if not .IsAnchor}}
|
||||
<form class="inline-form" hx-post="/setup/host/disconnect" hx-target="#setup" hx-swap="innerHTML">
|
||||
<input type="hidden" name="host" value="{{.Name}}">
|
||||
<label class="tick"><input type="checkbox" name="confirm" value="1"> disconnect {{.Name}}</label>
|
||||
<button class="no" type="submit">Disconnect</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="sub">No Host is connected. That is fine: the bastion watches this machine on its own. To connect one, use
|
||||
a token from your Host below.</p>
|
||||
{{end}}
|
||||
{{if and .Hosts (not .Whole)}}<p class="sub">Disconnecting a Host or releasing one from quarantine needs the
|
||||
machine's administrator; this bastion runs in your own account.</p>{{end}}
|
||||
</div>
|
||||
|
||||
<h3>Connect a Host</h3>
|
||||
<div class="host">
|
||||
<p class="sub">The token comes from your host's “Set up a bastion”. It is used once.</p>
|
||||
{{if .Limited}}<p class="sub">From your own account the console does not do this: it widens what reaches this machine and takes no code. The machine's administrator does it: <code>sudo -H privatae-bastion ui</code></p>{{else}}
|
||||
<form class="setup-form" hx-post="/setup/pair" hx-target="#setup" hx-swap="innerHTML"
|
||||
autocomplete="off">
|
||||
<label for="pair-token">Token</label>
|
||||
@@ -90,15 +163,17 @@
|
||||
is written <strong>pending</strong> and starts nothing until the anchor host
|
||||
approves it. Nothing here needs the bastion restarted.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* ── 2. second factor ──────────────────────────────────────────────────── */}}
|
||||
<h3>A second factor on this machine</h3>
|
||||
{{if .Hosts}}
|
||||
<h3>A code for each Host</h3>
|
||||
<div class="host">
|
||||
<p class="sub">Once a factor is enrolled, your host can only reach a folder or move
|
||||
files here after you approve the request on this machine. Your host never sees the code.</p>
|
||||
|
||||
{{if .Enrol}}
|
||||
{{if and .Enrol (not .Enrol.Machine)}}
|
||||
<div class="qr-wrap">
|
||||
{{/* The handle addresses the enrolment on the server. The secret is never
|
||||
in this page: it is in the pixels. */}}
|
||||
@@ -123,6 +198,8 @@
|
||||
<div id="stepup-key"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if and .Servable .Limited}}
|
||||
<p class="sub">From your own account the console does not do this: it widens what reaches this machine and takes no code. The machine's administrator does it: <code>sudo -H privatae-bastion ui</code></p>
|
||||
{{else if .Servable}}
|
||||
{{/* .Servable, not .Hosts: a pending pairing gets no state on this machine
|
||||
at all, not even a directory (run.go:311), and a factor for it would
|
||||
@@ -145,15 +222,17 @@
|
||||
<p class="sub">No host here has its lanes open, and a second factor gates ops a
|
||||
pairing without lanes cannot run. Nothing is created for a pending host on this
|
||||
machine until the anchor host approves it.</p>
|
||||
{{else}}
|
||||
<p class="sub">Pair a host first — a factor is enrolled per host.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Hosts}}
|
||||
{{/* ── 3. folders ────────────────────────────────────────────────────────── */}}
|
||||
<h3>Add a folder to your network</h3>
|
||||
<div class="host">
|
||||
{{if .Servable}}
|
||||
{{if and .Servable .Limited}}
|
||||
<p class="sub">From your own account the console does not do this: it widens what reaches this machine and takes no code. The machine's administrator does it: <code>sudo -H privatae-bastion ui</code></p>
|
||||
{{else if .Servable}}
|
||||
<p class="sub">Your files stay on this machine. Your host keeps only the index, and
|
||||
the draft does nothing until you approve it there.</p>
|
||||
<form class="setup-form" hx-post="/setup/folder/propose" hx-target="#setup" hx-swap="innerHTML"
|
||||
@@ -187,13 +266,17 @@
|
||||
propose a folder to yet.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Networked .AnchorHost .Hosts}}
|
||||
{{/* ── 4. the anchor ─────────────────────────────────────────────────────── */}}
|
||||
<h3>Which host holds the anchor</h3>
|
||||
<h3>Which Host is in charge (shared machines)</h3>
|
||||
<div class="host">
|
||||
<p class="sub">The anchor host is the one that approves every other host paired here.
|
||||
It is a choice among the hosts this machine already serves.</p>
|
||||
{{if .Hosts}}
|
||||
{{if and .Hosts .Limited}}
|
||||
<p class="sub">From your own account the console does not do this: it widens what reaches this machine and takes no code. The machine's administrator does it: <code>sudo -H privatae-bastion ui</code></p>
|
||||
{{else if .Hosts}}
|
||||
<form class="setup-form" hx-post="/setup/anchor/choose" hx-target="#anchor-cmd" hx-swap="innerHTML">
|
||||
{{range .Hosts}}
|
||||
<label class="tick">
|
||||
@@ -221,19 +304,26 @@
|
||||
and the act stays at a terminal.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── 5. the owner name ─────────────────────────────────────────────────── */}}
|
||||
<h3>Who owns this machine</h3>
|
||||
<div class="host">
|
||||
<p class="sub">A plain name, shown on this machine's card on every host it serves.
|
||||
It is optional and it grants nothing.</p>
|
||||
<form class="setup-form" hx-post="/setup/owner/name" hx-target="#owner-cmd" hx-swap="innerHTML"
|
||||
<form class="setup-form" hx-post="/setup/owner/name" hx-target="{{if and .Whole .OwnerCode}}#setup{{else}}#owner-cmd{{end}}" hx-swap="innerHTML"
|
||||
autocomplete="off">
|
||||
<label for="owner-name">Owner</label>
|
||||
<input id="owner-name" class="wide" name="owner" type="text" autocomplete="off"
|
||||
placeholder="{{if .Owner}}{{.Owner}}{{else}}a person or a team{{end}}">
|
||||
{{if and .Whole .OwnerCode}}
|
||||
<input class="code" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="your code" required>
|
||||
<button class="go" type="submit">Set the owner name</button>
|
||||
{{else if not .Whole}}
|
||||
<button type="submit">Show me the line to run</button>
|
||||
{{end}}
|
||||
</form>
|
||||
{{if and .Whole (not .OwnerCode)}}<p class="sub">Set up your code above, then set the name here.</p>{{end}}
|
||||
<div id="owner-cmd">{{if .Cmd}}{{template "owner-cmd" .}}{{end}}</div>
|
||||
<div class="note">
|
||||
Root-gated for the same reason as the anchor: there is no authority above this
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
<h3>Hosts this machine serves</h3>
|
||||
{{if not .Pairings}}
|
||||
<div class="host">
|
||||
<div class="host-name">No host is paired yet.</div>
|
||||
<p class="sub">Pair one with a token from your host's “Set up a bastion”.</p>
|
||||
<div class="host-name">No Host is connected.</div>
|
||||
<p class="sub">That is fine: the bastion watches this machine on its own. Connecting a Host is optional, under <a href="/setup">Settings</a>.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{range .Pairings}}
|
||||
|
||||
Reference in New Issue
Block a user