@jbarket/loomai 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -1
- package/README.md +28 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/household.d.ts +3 -0
- package/dist/cli/household.js +139 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/serve.js +9 -2
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +1 -1
- package/dist/config.js +5 -0
- package/dist/household.d.ts +101 -0
- package/dist/household.js +389 -0
- package/dist/injection/writer.js +5 -2
- package/dist/path-safety.d.ts +1 -1
- package/dist/path-safety.js +5 -3
- package/dist/server.d.ts +7 -0
- package/dist/server.js +2 -2
- package/dist/tools/harness.d.ts +17 -0
- package/dist/tools/harness.js +45 -2
- package/dist/transport/http-server.d.ts +7 -0
- package/dist/transport/http-server.js +147 -20
- package/dist/ui/handler.d.ts +15 -2
- package/dist/ui/handler.js +22 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.10.0] - 2026-10-24
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- **`harness_init` could write any file the daemon's user can.** Its
|
|
20
|
+
`target` was a free path and the harness name could carry newlines, so any
|
|
21
|
+
MCP caller could append to another agent's `IDENTITY.md` or plant a command
|
|
22
|
+
in a shell rc file. Over HTTP the target is now refused: it names a file on
|
|
23
|
+
the server, never the caller's. Locally it must be a harness instruction
|
|
24
|
+
file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, …), judged at its real path.
|
|
25
|
+
Names refuse control characters, and managed-block writes use a random,
|
|
26
|
+
exclusive temp file. Found by the security review of the household work.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **The household, phase 1: one loom, several agents**
|
|
31
|
+
(docs/specs/household.md).
|
|
32
|
+
- `$LOOM_HOME/household.json` lists the agents and the default, and
|
|
33
|
+
`tokens.json` holds every token by sha256 with a label and a role.
|
|
34
|
+
- **Roles:** `agent:<name>` is MCP and `/cli` as that agent's stack only.
|
|
35
|
+
`owner` gets the default agent, another at `/agents/<name>/`, and the
|
|
36
|
+
page. `peer:<name>` gets knowledge tools only.
|
|
37
|
+
- **On every request** the daemon resolves the role and builds the
|
|
38
|
+
session's server over that agent's context dir. A session stays pinned
|
|
39
|
+
to the role and agent it opened with, and a revoke takes effect on the
|
|
40
|
+
next request.
|
|
41
|
+
- **New commands:** `loom household init|add|show` and
|
|
42
|
+
`loom token create|list|revoke`.
|
|
43
|
+
- **Nothing changes for one agent:** a plain context dir is a household of
|
|
44
|
+
one and behaves exactly as before.
|
|
45
|
+
|
|
15
46
|
## [0.9.2] - 2026-10-24
|
|
16
47
|
|
|
17
48
|
### Fixed
|
|
@@ -666,7 +697,8 @@ Initial public release.
|
|
|
666
697
|
stack and all added external-service dependencies or operational
|
|
667
698
|
overhead.
|
|
668
699
|
|
|
669
|
-
[Unreleased]: https://github.com/sleepunit-agents/loom/compare/v0.
|
|
700
|
+
[Unreleased]: https://github.com/sleepunit-agents/loom/compare/v0.10.0...HEAD
|
|
701
|
+
[0.10.0]: https://github.com/sleepunit-agents/loom/compare/v0.9.2...v0.10.0
|
|
670
702
|
[0.9.2]: https://github.com/sleepunit-agents/loom/compare/v0.9.1...v0.9.2
|
|
671
703
|
[0.9.1]: https://github.com/sleepunit-agents/loom/compare/v0.9.0...v0.9.1
|
|
672
704
|
[0.9.0]: https://github.com/sleepunit-agents/loom/compare/v0.8.0...v0.9.0
|
package/README.md
CHANGED
|
@@ -648,6 +648,34 @@ loom tasks ready # → loom-tasks ready
|
|
|
648
648
|
The rules, the dispatch details and how pieces compose:
|
|
649
649
|
[`docs/extensions.md`](docs/extensions.md).
|
|
650
650
|
|
|
651
|
+
### The household — several agents on one loom
|
|
652
|
+
|
|
653
|
+
One loom can serve several agents, with a shared commons
|
|
654
|
+
([`docs/specs/household.md`](docs/specs/household.md)). A household is a
|
|
655
|
+
directory (`$LOOM_HOME`, default `~/.config/loom`) with a `household.json`,
|
|
656
|
+
a `tokens.json`, and one context dir per agent under `agents/`. A plain
|
|
657
|
+
context dir with no household around it is a household of one and works
|
|
658
|
+
exactly as before.
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
loom household init art # ~/.config/loom/art → agents/art (a symlink stays)
|
|
662
|
+
loom household add mark # an agent whose dir already exists under agents/
|
|
663
|
+
loom token create --agent art --label "Art · house on wunder" # printed once
|
|
664
|
+
loom token create --owner --label "Jonathan · page"
|
|
665
|
+
loom token list
|
|
666
|
+
loom token revoke "Art · house on wunder"
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Every token has a role. **`agent:<name>`** is MCP and `/cli` as that agent:
|
|
670
|
+
its own stack only. **`owner`** gets the default agent, or another at
|
|
671
|
+
`/agents/<name>/` on the daemon, and the page. **`peer:<name>`** gets
|
|
672
|
+
knowledge tools only. The daemon resolves the role on every request; no tool
|
|
673
|
+
takes an agent argument, so a body can only be the agent its token names.
|
|
674
|
+
`LOOM_BEARER_TOKEN`, when set, is an owner token too. A daemon whose context
|
|
675
|
+
dir is `<home>/agents/<name>` next to a `household.json` serves the whole
|
|
676
|
+
household. A process with `LOOM_HOME` set and no context dir uses
|
|
677
|
+
`LOOM_AGENT`, or the household's default.
|
|
678
|
+
|
|
651
679
|
### Harness manifests and self-describe
|
|
652
680
|
|
|
653
681
|
A **harness** is the MCP-capable runtime the agent runs in (Claude Code,
|
package/dist/cli/args.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { resolveDefaultContextPath } from '../config.js';
|
|
8
|
+
import { contextFromHome } from '../household.js';
|
|
8
9
|
export function resolveEnv(flags, processEnv) {
|
|
9
10
|
const contextDir = flags.contextDir ??
|
|
10
11
|
processEnv.LOOM_CONTEXT_DIR ??
|
|
12
|
+
contextFromHome(processEnv) ??
|
|
11
13
|
resolveDefaultContextPath(processEnv.HOME);
|
|
12
14
|
return {
|
|
13
15
|
contextDir: resolve(contextDir),
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loom household` and `loom token` (docs/specs/household.md). Local only:
|
|
3
|
+
* neither is a remote verb, so they never run through /cli. They read and
|
|
4
|
+
* write $LOOM_HOME (default ~/.config/loom) directly.
|
|
5
|
+
*/
|
|
6
|
+
import { addAgent, createToken, householdHome, HouseholdError, initHousehold, loadHousehold, loadTokens, parseRole, revokeToken, } from '../household.js';
|
|
7
|
+
const HOUSEHOLD_USAGE = `Usage: loom household <command>
|
|
8
|
+
|
|
9
|
+
show The household at $LOOM_HOME: agents, the default, the owner
|
|
10
|
+
init <agent> [--owner <name>]
|
|
11
|
+
Make $LOOM_HOME/<agent> (a context dir) the first agent of a new
|
|
12
|
+
household: it moves to agents/<agent>, and a symlink stays behind
|
|
13
|
+
add <agent> Add an agent whose directory already exists under agents/
|
|
14
|
+
|
|
15
|
+
$LOOM_HOME defaults to ~/.config/loom.
|
|
16
|
+
`;
|
|
17
|
+
const TOKEN_USAGE = `Usage: loom token <command>
|
|
18
|
+
|
|
19
|
+
create (--agent <name> | --owner | --peer <name>) --label <text>
|
|
20
|
+
Mint a token. It's printed once; only its sha256 is kept
|
|
21
|
+
list Every token: label, role, created, last used, revoked
|
|
22
|
+
revoke <label> Revoke the live token with that label (takes effect on the next request)
|
|
23
|
+
|
|
24
|
+
Roles: agent:<name> is MCP and the CLI as that agent; owner sees everything and
|
|
25
|
+
the page; peer:<name> reaches commons knowledge only.
|
|
26
|
+
`;
|
|
27
|
+
function flag(args, name) {
|
|
28
|
+
const i = args.indexOf(name);
|
|
29
|
+
return i === -1 ? undefined : args[i + 1];
|
|
30
|
+
}
|
|
31
|
+
function need(io) {
|
|
32
|
+
const home = householdHome(io.env);
|
|
33
|
+
const h = loadHousehold(home);
|
|
34
|
+
if (!h)
|
|
35
|
+
io.stderr(`No household at ${home} (no household.json). Make one with: loom household init <agent>\n`);
|
|
36
|
+
return h;
|
|
37
|
+
}
|
|
38
|
+
export async function runHousehold(argv, io) {
|
|
39
|
+
const [sub, ...rest] = argv;
|
|
40
|
+
try {
|
|
41
|
+
if (sub === 'show') {
|
|
42
|
+
const h = need(io);
|
|
43
|
+
if (!h)
|
|
44
|
+
return 1;
|
|
45
|
+
io.stdout(`home ${h.home}\nagents ${h.agents.join(', ')}\ndefault ${h.default}\n${h.owner ? `owner ${h.owner}\n` : ''}`);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'init') {
|
|
49
|
+
const agent = rest[0];
|
|
50
|
+
if (!agent || agent.startsWith('-')) {
|
|
51
|
+
io.stderr(HOUSEHOLD_USAGE);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
const h = initHousehold(householdHome(io.env), agent, { owner: flag(rest, '--owner') });
|
|
55
|
+
io.stdout(`Household at ${h.home}: ${agent} moved to agents/${agent}; ${h.home}/${agent} is now a symlink to it.\n`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (sub === 'add') {
|
|
59
|
+
const h = need(io);
|
|
60
|
+
if (!h)
|
|
61
|
+
return 1;
|
|
62
|
+
const agent = rest[0];
|
|
63
|
+
if (!agent) {
|
|
64
|
+
io.stderr(HOUSEHOLD_USAGE);
|
|
65
|
+
return 2;
|
|
66
|
+
}
|
|
67
|
+
const next = addAgent(h, agent);
|
|
68
|
+
io.stdout(`Agents: ${next.agents.join(', ')}\n`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
io.stderr(HOUSEHOLD_USAGE);
|
|
72
|
+
return sub === undefined || sub === '--help' || sub === '-h' ? 0 : 2;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof HouseholdError) {
|
|
76
|
+
io.stderr(`${err.message}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function runToken(argv, io) {
|
|
83
|
+
const [sub, ...rest] = argv;
|
|
84
|
+
try {
|
|
85
|
+
if (sub === 'create') {
|
|
86
|
+
const h = need(io);
|
|
87
|
+
if (!h)
|
|
88
|
+
return 1;
|
|
89
|
+
const label = flag(rest, '--label');
|
|
90
|
+
const picked = [flag(rest, '--agent') && `agent:${flag(rest, '--agent')}`, rest.includes('--owner') && 'owner', flag(rest, '--peer') && `peer:${flag(rest, '--peer')}`].filter(Boolean);
|
|
91
|
+
const role = picked.length === 1 ? parseRole(picked[0]) : null;
|
|
92
|
+
if (!label || !role) {
|
|
93
|
+
io.stderr(TOKEN_USAGE);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const { token, record } = createToken(h, { role, label });
|
|
97
|
+
// The token goes to stdout alone, so it can be piped straight into a file.
|
|
98
|
+
io.stderr(`Created ${record.role} token "${record.label}". It won't be shown again.\n`);
|
|
99
|
+
io.stdout(`${token}\n`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (sub === 'list') {
|
|
103
|
+
const h = need(io);
|
|
104
|
+
if (!h)
|
|
105
|
+
return 1;
|
|
106
|
+
const tokens = loadTokens(h.home);
|
|
107
|
+
if (!tokens.length) {
|
|
108
|
+
io.stdout('No tokens yet.\n');
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
for (const t of tokens) {
|
|
112
|
+
io.stdout(`${t.revoked ? 'revoked' : 'live '} ${t.role.padEnd(16)} ${t.label} (created ${t.created.slice(0, 10)}${t.last_used ? `, last used ${t.last_used.slice(0, 16).replace('T', ' ')}` : ', never used'}${t.revoked ? `, revoked ${t.revoked.slice(0, 10)}` : ''})\n`);
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
if (sub === 'revoke') {
|
|
117
|
+
const h = need(io);
|
|
118
|
+
if (!h)
|
|
119
|
+
return 1;
|
|
120
|
+
const label = rest.join(' ').trim();
|
|
121
|
+
if (!label) {
|
|
122
|
+
io.stderr(TOKEN_USAGE);
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
const t = revokeToken(h, label);
|
|
126
|
+
io.stdout(`Revoked ${t.role} token "${t.label}".\n`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
io.stderr(TOKEN_USAGE);
|
|
130
|
+
return sub === undefined || sub === '--help' || sub === '-h' ? 0 : 2;
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err instanceof HouseholdError) {
|
|
134
|
+
io.stderr(`${err.message}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -32,6 +32,8 @@ Commands:
|
|
|
32
32
|
doctor Probe the loom environment (read-only)
|
|
33
33
|
migrate Apply pending schema migrations to memories.db
|
|
34
34
|
pieces List installed pieces (loom-<name> on PATH)
|
|
35
|
+
household The household: agents and the default (docs/specs/household.md)
|
|
36
|
+
token Mint, list and revoke the household's tokens
|
|
35
37
|
|
|
36
38
|
Pieces: \`loom <name> ...\` runs an installed extension, loom-<name> on PATH
|
|
37
39
|
(e.g. loom tasks list). Core commands always win. See docs/extensions.md.
|
|
@@ -198,6 +200,14 @@ async function dispatch(sub, rest, io) {
|
|
|
198
200
|
const { runPiecesCommand } = await import('./pieces.js');
|
|
199
201
|
return runPiecesCommand(rest, io);
|
|
200
202
|
}
|
|
203
|
+
case 'household': {
|
|
204
|
+
const { runHousehold } = await import('./household.js');
|
|
205
|
+
return runHousehold(rest, io);
|
|
206
|
+
}
|
|
207
|
+
case 'token': {
|
|
208
|
+
const { runToken } = await import('./household.js');
|
|
209
|
+
return runToken(rest, io);
|
|
210
|
+
}
|
|
201
211
|
default:
|
|
202
212
|
io.stderr(`Subcommand not implemented yet: ${sub}\n`);
|
|
203
213
|
return 2;
|
package/dist/cli/serve.js
CHANGED
|
@@ -17,6 +17,7 @@ import { parseArgs } from 'node:util';
|
|
|
17
17
|
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
18
18
|
import { createLoomServer } from '../server.js';
|
|
19
19
|
import { createKnowledgeOnlyServer } from '../transport/knowledge-server.js';
|
|
20
|
+
import { householdOf } from '../household.js';
|
|
20
21
|
import { startHttpServer } from '../transport/http-server.js';
|
|
21
22
|
import { resolveContextDir } from '../config.js';
|
|
22
23
|
const USAGE = `Usage: loom serve [options]
|
|
@@ -140,9 +141,15 @@ export async function run(argv, io) {
|
|
|
140
141
|
const host = values.host ?? process.env.LOOM_HTTP_HOST ?? '127.0.0.1';
|
|
141
142
|
const port = Number(values.port ?? process.env.LOOM_HTTP_PORT ?? 8787);
|
|
142
143
|
const token = process.env.LOOM_BEARER_TOKEN || undefined;
|
|
143
|
-
|
|
144
|
+
// A context dir at <home>/agents/<name> next to a household.json serves
|
|
145
|
+
// the whole household (docs/specs/household.md); anything else is a
|
|
146
|
+
// household of one, exactly as before.
|
|
147
|
+
const found = householdOf(contextDir);
|
|
148
|
+
const handle = await startHttpServer({ contextDir, host, port, token, gapsDir, household: found?.household });
|
|
144
149
|
io.stderr(`loom: HTTP MCP daemon on http://${handle.host}:${handle.port} ` +
|
|
145
|
-
|
|
150
|
+
(found
|
|
151
|
+
? `(household of ${found.household.agents.join(', ')}; tokens in ${found.household.home}/tokens.json)\n`
|
|
152
|
+
: `(${token ? 'bearer-gated' : 'open — network is the boundary'})\n`));
|
|
146
153
|
// Hold open until the process is signalled.
|
|
147
154
|
await new Promise(() => { });
|
|
148
155
|
return 0;
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* (src/cli/index.ts) and the CLI-vs-MCP routing gate (src/index.ts) so
|
|
4
4
|
* the set never drifts between entry points.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate', 'pieces'];
|
|
6
|
+
export declare const SUBCOMMANDS: readonly ['wake', 'recall', 'remember', 'forget', 'update', 'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve', 'inject', 'harness', 'install', 'doctor', 'migrate', 'pieces', 'household', 'token'];
|
|
7
7
|
export type Subcommand = typeof SUBCOMMANDS[number];
|
package/dist/cli/subcommands.js
CHANGED
|
@@ -7,5 +7,5 @@ export const SUBCOMMANDS = [
|
|
|
7
7
|
'wake', 'recall', 'remember', 'forget', 'update',
|
|
8
8
|
'memory', 'knowledge', 'person', 'update-identity', 'bootstrap', 'serve',
|
|
9
9
|
'inject', 'harness',
|
|
10
|
-
'install', 'doctor', 'migrate', 'pieces',
|
|
10
|
+
'install', 'doctor', 'migrate', 'pieces', 'household', 'token',
|
|
11
11
|
];
|
package/dist/config.js
CHANGED
|
@@ -18,6 +18,7 @@ import { resolve, dirname, basename } from 'node:path';
|
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, statSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { contextFromHome } from './household.js';
|
|
21
22
|
/**
|
|
22
23
|
* The single canonical helper for the default context path. Every call
|
|
23
24
|
* site that needs `~/.config/loom/default` must go through this —
|
|
@@ -34,6 +35,10 @@ export function resolveContextDir() {
|
|
|
34
35
|
if (argIdx !== -1 && process.argv[argIdx + 1]) {
|
|
35
36
|
return resolve(process.argv[argIdx + 1]);
|
|
36
37
|
}
|
|
38
|
+
// LOOM_HOME with a household.json: the agent LOOM_AGENT names, or its default.
|
|
39
|
+
const fromHome = contextFromHome();
|
|
40
|
+
if (fromHome)
|
|
41
|
+
return fromHome;
|
|
37
42
|
return resolveDefaultContextPath();
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export declare const AGENT_NAME: RegExp;
|
|
2
|
+
export interface Household {
|
|
3
|
+
/** $LOOM_HOME: where household.json lives. */
|
|
4
|
+
home: string;
|
|
5
|
+
agents: string[];
|
|
6
|
+
/** The agent the owner's token acts as when it names none. */
|
|
7
|
+
default: string;
|
|
8
|
+
owner?: string;
|
|
9
|
+
}
|
|
10
|
+
export type Role = {
|
|
11
|
+
kind: 'agent';
|
|
12
|
+
agent: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'owner';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'peer';
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
export interface TokenRecord {
|
|
20
|
+
label: string;
|
|
21
|
+
/** `agent:<name>`, `owner` or `peer:<name>`. */
|
|
22
|
+
role: string;
|
|
23
|
+
/** sha256 of the token, hex. The token itself is never stored. */
|
|
24
|
+
sha256: string;
|
|
25
|
+
created: string;
|
|
26
|
+
last_used?: string;
|
|
27
|
+
revoked?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class HouseholdError extends Error {
|
|
30
|
+
}
|
|
31
|
+
/** $LOOM_HOME, else ~/.config/loom. */
|
|
32
|
+
export declare function householdHome(env?: NodeJS.ProcessEnv): string;
|
|
33
|
+
export declare function agentDir(h: Household, agent: string): string;
|
|
34
|
+
/** The household at `home`, or null when there's none. Throws on a malformed file. */
|
|
35
|
+
export declare function loadHousehold(home: string): Household | null;
|
|
36
|
+
/**
|
|
37
|
+
* The household a context dir belongs to: one whose `agents/<name>` is this
|
|
38
|
+
* dir (after resolving symlinks, so an old `~/.config/loom/art` link still
|
|
39
|
+
* finds it). Null for a plain context dir.
|
|
40
|
+
*/
|
|
41
|
+
export declare function householdOf(contextDir: string): {
|
|
42
|
+
household: Household;
|
|
43
|
+
agent: string;
|
|
44
|
+
} | null;
|
|
45
|
+
export declare function parseRole(s: string): Role | null;
|
|
46
|
+
export declare function roleString(r: Role): string;
|
|
47
|
+
export declare function loadTokens(home: string): TokenRecord[];
|
|
48
|
+
/** Mint a token. Returns it once; only its sha256 is kept. */
|
|
49
|
+
export declare function createToken(h: Household, input: {
|
|
50
|
+
role: Role;
|
|
51
|
+
label: string;
|
|
52
|
+
}, now?: Date): {
|
|
53
|
+
token: string;
|
|
54
|
+
record: TokenRecord;
|
|
55
|
+
};
|
|
56
|
+
export declare function revokeToken(h: Household, label: string, now?: Date): TokenRecord;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves presented tokens to roles. Reads tokens.json when it changes (so
|
|
59
|
+
* a revoke takes effect on the next request), compares digests in constant
|
|
60
|
+
* time, and records when a token was last used, at most once a minute.
|
|
61
|
+
*/
|
|
62
|
+
export declare class TokenResolver {
|
|
63
|
+
private readonly h;
|
|
64
|
+
private cache;
|
|
65
|
+
private lastTouch;
|
|
66
|
+
constructor(h: Household);
|
|
67
|
+
private current;
|
|
68
|
+
/**
|
|
69
|
+
* The role for a presented token, or null. Revoked tokens and roles naming a
|
|
70
|
+
* missing agent resolve to null. Throws HouseholdError when tokens.json is
|
|
71
|
+
* unreadable or untrusted: the caller fails closed.
|
|
72
|
+
*/
|
|
73
|
+
resolve(presented: string): Role | null;
|
|
74
|
+
/** Whether a live owner token has this sha256 (the page's sessions check this, so revoking ends them). */
|
|
75
|
+
ownerDigestLive(digestHex: string): boolean;
|
|
76
|
+
private touch;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Turn a plain context dir into the first agent of a new household: move
|
|
80
|
+
* `<home>/<name>` to `<home>/agents/<name>`, leave a symlink at the old path
|
|
81
|
+
* so anything that still names it keeps working, and write household.json.
|
|
82
|
+
*/
|
|
83
|
+
export declare function initHousehold(home: string, agent: string, opts?: {
|
|
84
|
+
owner?: string;
|
|
85
|
+
}): Household;
|
|
86
|
+
/**
|
|
87
|
+
* An agent's directory must be a real directory, not a symlink: `agents/mark
|
|
88
|
+
* -> art` would make two agents share one stack.
|
|
89
|
+
*/
|
|
90
|
+
export declare function assertRealAgentDir(h: Household, agent: string): void;
|
|
91
|
+
/** Add an agent to a household (its directory must already exist under agents/). */
|
|
92
|
+
export declare function addAgent(h: Household, agent: string): Household;
|
|
93
|
+
/**
|
|
94
|
+
* The context dir a household names when nothing else does: with LOOM_HOME
|
|
95
|
+
* set and a household.json there, `agents/<LOOM_AGENT or the default>`.
|
|
96
|
+
* Undefined otherwise. Only an explicit LOOM_HOME is consulted here, so a
|
|
97
|
+
* stray household.json never changes where an unconfigured process looks.
|
|
98
|
+
* (A daemon is different: one whose context dir already is
|
|
99
|
+
* `<home>/agents/<name>` serves that household; see householdOf.)
|
|
100
|
+
*/
|
|
101
|
+
export declare function contextFromHome(env?: NodeJS.ProcessEnv): string | undefined;
|