@tomato414941/foundation 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/guide.mjs +33 -23
  2. package/package.json +1 -1
  3. package/runtime.mjs +15 -10
package/guide.mjs CHANGED
@@ -26,24 +26,33 @@ export function guide(connectors) {
26
26
  'Nothing here needs a shell. With one, use foundation api to attach the key and foundation exec to deliver values',
27
27
  'to a command. api prints responses: use function save options when output should not enter your context.',
28
28
  'Never ask the owner to paste passwords, keys or tokens into chat. Use a storage request or OAuth URL instead.', '',
29
- 'YOUR KEY',
30
- ' Your key is a secret of your own making: 43 URL-safe base64 characters after `fdn_`. Generate one, keep it private,',
29
+ 'WHO YOU ARE',
30
+ ' Everything that comes to Foundation is a principal: a person, an AI, an app. You are one, and your key is what',
31
+ ' proves it: a secret of your own making, 43 URL-safe base64 characters after `fdn_`. Generate one, keep it private,',
31
32
  ' and never print it. If you can write a private file, keep it there; otherwise keep it wherever your secrets go.',
32
- ' POST /v1/keys {"name": "<what to call you>"} with your key as the bearer.',
33
- ' Returns verification_uri and confirmation_code. Give the owner both; they open the URL and type the code.',
34
- ' Until they do, every other call answers 401 not_approved. Retry GET /v1/keys/current every few seconds. Do not hammer it.',
35
- ' GET /v1/keys/current your request and what happened at its page; once approved, also who you are (key).',
36
- ' Approval also has kind=approve, status=done and result.key_id. A current key, not request completion, authorizes use.',
37
- ' Revoking a key stops Foundation access; credentials already obtained must be revoked at their provider.',
38
- ' PATCH /v1/keys/current {"name"} DELETE /v1/keys/current cancels the request, or revokes your approved key.', '',
33
+ ' What you may reach follows from lines between principals: whoever you act for (actor), whoever made you (owner).',
34
+ ' POST /v1/requests {"kind": "actor", "input": {"name": "<what to call you>"}} with your key as the bearer.',
35
+ ' Asks whoever opens it to let you act for them. Returns verification_uri and confirmation_code. Give the person',
36
+ ' both; they open the URL and type the code. Until they do you act for nobody: everything below answers as',
37
+ ' your own, empty, holdings. Retry GET /v1/principals/me every few seconds and look at acts_for. Do not hammer it.',
38
+ ' GET /v1/principals/me who you are: principal, acts_for (whom you act for), owners, credentials, your open requests.',
39
+ ' Acting for exactly one principal, their holdings are what the calls below reach. For several, name one: ?as=<id>.',
40
+ ' PATCH /v1/principals/me {"name"} DELETE /v1/principals/me a new name; leaving (your key stops working).',
41
+ ' GET /v1/requests/<id> one of your requests and what happened at its page (events). DELETE cancels it.',
42
+ ' GET /v1/relations every line you are on. GET /v1/records what was done in your name or to you.',
43
+ ' Everything kept has an id, shown in every listing. The holder shows one thing to another principal with',
44
+ ' POST /v1/relations {"subject":"<principal id>","relation":"viewer"|"editor","object_type":"holding","object_id":"<id>"} DELETE takes it back.',
45
+ ' GET /v1/holdings what others have shown you. GET /v1/holdings/<id> one thing, whoever holds it.',
46
+ ' GET /v1/holdings/<id>/content reads it; PUT writes it, as editor. Lines point at the id, so renaming changes nothing.', '',
39
47
  'WHAT IS KEPT',
40
- ' PUT /v1/secrets?name=<name>&secret=true body: raw bytes, up to 1MB. The same exact name replaces that value.',
41
- ' secret=true blocks direct GET by an access key. The owner can read it; authorized delivery can still return it.',
42
- ' This is not a boundary against an agent with the approved key. Do not print delivered values into its context.',
43
- ' GET /v1/secrets names, sizes, read permissions and timestamps, not contents.',
44
- ' GET /v1/secrets?prefix=<literal prefix> optional case-sensitive text filtering, not a directory.',
45
- ' GET /v1/secrets?name=<name> bytes, as written; 403 write_only for a secret.',
46
- ' DELETE /v1/secrets?name=<name>',
48
+ ' PUT /v1/holdings?kind=secret&name=<name> body: raw bytes, up to 1MB. The same exact name replaces that value; the answer carries its id.',
49
+ ' What you keep for the holder you may read back (an editor line is drawn for you). What the holder kept, or a',
50
+ ' connection handed over, you may deliver into a command but not read, unless the holder draws you a line.',
51
+ ' Do not print delivered values into your context.',
52
+ ' GET /v1/holdings?kind=secret ids, names, sizes and timestamps, not contents. &prefix=<literal prefix> narrows by text, not directory.',
53
+ ' GET /v1/holdings?kind=secret&name=<name> the one thing by name: its id and metadata. 404 if no such name.',
54
+ ' GET /v1/holdings/<id>/content bytes, as written; 403 forbidden without a line to it. PUT writes them.',
55
+ ' PATCH /v1/holdings/<id> {"name"} a new name for the same thing. DELETE /v1/holdings/<id> removes it.',
47
56
  ' URL-encode the name. A name is any text, which is why it travels as a query and not as a path. Names are 1-200 characters without control characters; case, spaces, slashes and punctuation',
48
57
  ' remain literal. No normalization, hierarchy, service ownership or automatic renewal is inferred.',
49
58
  ' The owner can read, rename or delete any saved value. Stored copies survive OAuth disconnection.', '',
@@ -56,7 +65,7 @@ export function guide(connectors) {
56
65
  ' Calling this into an agent context exposes values. Use exec when the intent is to run a local command.', '',
57
66
  'FILLING THE STORE',
58
67
  '',
59
- '1. Put it there yourself. Anything you obtained or wrote: PUT /v1/secrets?name=<name>, above.',
68
+ '1. Put it there yourself. Anything you obtained or wrote: PUT /v1/holdings?kind=secret&name=<name>, above.',
60
69
  '',
61
70
  '2. ASKING THE OWNER, for what only they can fetch -- an API token, a key, a certificate they must go and create.',
62
71
  ' POST /v1/requests {"kind":"store", "input":{"fields":[{...}]}, "purpose":"...", "steps":["..."], "valid_minutes":30}',
@@ -64,7 +73,7 @@ export function guide(connectors) {
64
73
  ' fields[].label what they are being asked for, in their language. It titles the screen and names the field.',
65
74
 
66
75
  ' fields[].site the page where they make it, offered as a link',
67
- ' fields[].secret false lets you read it back afterwards; the default is that you cannot',
76
+ ' fields[].readable true asks to read it back afterwards (a viewer line); the default is that you cannot',
68
77
  ' fields[].multiline true for something like a PEM',
69
78
  ' fields[].replace true to put the value in place of the one already kept under that exact name (rotation).',
70
79
  ' Refused at creation: name_taken when the name exists and replace is not declared, name_missing',
@@ -103,11 +112,12 @@ export function guide(connectors) {
103
112
  ' Request feedback expires with the request. A done result is a connection_id or the names saved at completion.',
104
113
  ' Why a registration failed: invalid_values (wrong shape) / invalid_credential (the connector would not take it) /',
105
114
  ' reconnect_required (the service rejected it) / already_connected. A key approval shows its own events at',
106
- ' GET /v1/keys/current: confirmation_required (a wrong code) / confirmation_locked (5 tries).', '',
115
+ ' GET /v1/requests/<id>: confirmation_required (a wrong code) / confirmation_locked (5 tries).', '',
107
116
  'A PLACE FOR FILES (object storage the owner did not have to sign up for)',
108
- ' PUT /v1/objects/<key> body is the bytes; the Content-Type you send is what a reader gets back. Up to 25MB.',
109
- ' GET /v1/objects/<key> the bytes. DELETE removes it. GET /v1/objects?prefix=<p>&cursor=<c> lists what is there.',
110
- ' POST /v1/objects/<key>/link {"minutes":n} a time-limited URL anyone can read, for a thing that only takes a URL.',
117
+ ' PUT /v1/holdings?kind=object&name=<key> body is the bytes; the Content-Type you send is what a reader gets back. Up to 25MB.',
118
+ ' GET /v1/holdings?kind=object&prefix=<p> lists what is there, with ids. &name=<key> finds one.',
119
+ ' GET /v1/holdings/<id>/content the bytes. PUT writes them. DELETE /v1/holdings/<id> removes it. PATCH renames it.',
120
+ ' POST /v1/holdings/<id>/link {"minutes":n} a time-limited URL anyone can read, for a thing that only takes a URL.',
111
121
  ' Keys look like a path (a/b/c.txt) but name one whole object: no renaming, no directories, no partial reads or writes.',
112
122
  ' This is not a filesystem. If the owner needs one, they need a machine to mount it on.',
113
123
  ' Where the bytes live is the owner\'s business: a space lent to them now, a bucket of their own later. Nothing you call changes.', '',
@@ -139,7 +149,7 @@ export function guide(connectors) {
139
149
  ' Use this instead of POST /v1/deliveries whenever the point is to run something.',
140
150
  ` foundation exec --output '{"name":"login config","as":"AUTH_FILE","filename":"auth.json"}' -- <command>`,
141
151
  ' Creates one empty private file and sets as to its path. Tell the command to write its authentication result there.',
142
- ' After exit 0, saves the file bytes under the exact name with secret=true, then removes the temporary file.',
152
+ ' After exit 0, saves the file bytes under the exact name, then removes the temporary file.',
143
153
  ' An existing value at that name is replaced. Output must be a private regular file containing 1 byte to 1MB.',
144
154
  ' May be combined with inputs, using a different environment variable. A failed command never saves its output.',
145
155
  ' If upload cannot be confirmed, exits with an error and reports the retained private file for recovery; inputs are',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomato414941/foundation",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Foundation CLI: make a key, and hand what is kept to a command without it passing through the agent.",
5
5
  "type": "module",
6
6
  "engines": {
package/runtime.mjs CHANGED
@@ -206,20 +206,22 @@ async function main() {
206
206
  // Asking the owner to approve this key. The key itself is never printed: it stays in the file.
207
207
  // A key the owner already approved has nothing to ask; connecting again only changes which server is remembered.
208
208
  if (action === 'connect') {
209
- const answer = await send('/v1/keys', { name: name ?? hostname() + ' の ' + (agentName || 'AI') }, { accept: data => data.error?.code === 'already_approved' });
209
+ // A key someone already accepted has nothing to ask; connecting again only changes which server is remembered.
210
+ let me = null;
211
+ try { me = await send('/v1/principals/me', undefined, { method: 'GET' }); } catch {}
212
+ const answer = me?.acts_for?.length ? null : await send('/v1/requests', { kind: 'actor', input: { name: name ?? hostname() + ' の ' + (agentName || 'AI') } });
210
213
  if (connectTo !== undefined) await saveUrl(url.origin);
211
- console.log(answer.error ? 'Already approved on ' + url.origin + '.' : JSON.stringify(answer, null, 2));
214
+ console.log(answer === null ? 'Already approved on ' + url.origin + '.' : JSON.stringify(answer, null, 2));
212
215
  console.log('\nKey file: ' + keyPath + '\nServer: ' + url.origin + (connectTo !== undefined ? ' (saved to ' + configPath() + ')' : '') + '\nEverything else is HTTP: Authorization: Bearer <the contents of that file>');
213
216
  return;
214
217
  }
215
- // Even output-only commands need an approved key before they start an external login.
218
+ // Nothing runs before someone has accepted this key: a key that acts for nobody reaches only its own empty holdings,
219
+ // and the person it asked has yet to answer.
220
+ const current = await send('/v1/principals/me', undefined, { method: 'GET' });
221
+ if (!current.acts_for?.length) throw new Error('Foundation request failed (401, not_approved). This key acts for nobody yet' + (current.requests?.[0] ? '; it is waiting for approval at ' + current.requests[0].verification_uri : '') + '.');
216
222
  let delivery;
217
223
  if (names.length) ({ delivery } = await send('/v1/deliveries', { names }));
218
- else {
219
- const current = await send('/v1/keys/current', undefined, { method: 'GET' });
220
- if (!current.key) throw new Error('Foundation request failed (401, not_approved). This key is waiting for approval at ' + current.request?.verification_uri + '.');
221
- delivery = { environment: {}, files: [] };
222
- }
224
+ else delivery = { environment: {}, files: [] };
223
225
  if (!delivery || typeof delivery.environment !== 'object' || !Array.isArray(delivery.files)) throw new Error('Foundation returned an invalid delivery.');
224
226
  // What each of them sets is the server's to say; this applies it and refuses anything it may not set.
225
227
  const environment = { ...process.env };
@@ -248,7 +250,7 @@ async function main() {
248
250
  }
249
251
  }
250
252
  };
251
- const recovery = () => 'Foundation could not confirm the output was saved. The private output file is retained for recovery: ' + outputPath + '\nRetry with foundation api PUT "/v1/secrets?name=<URL-encoded-name>&secret=true" --from <file>, then remove that recovery file.';
253
+ const recovery = () => 'Foundation could not confirm the output was saved. The private output file is retained for recovery: ' + outputPath + '\nRetry with foundation api PUT "/v1/holdings?kind=secret&name=<URL-encoded-name>" --from <file>, then remove that recovery file.';
252
254
  process.once('exit', cleanup);
253
255
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(signal, () => {
254
256
  interrupted = true;
@@ -285,8 +287,11 @@ async function main() {
285
287
  if (output && process.exitCode === 0) {
286
288
  const bytes = await outputBytes(outputPath);
287
289
  retainOutput = true;
288
- try { await send('/v1/secrets?name=' + encodeURIComponent(output.name) + '&secret=true', bytes, { method: 'PUT', type: 'application/octet-stream' }); }
290
+ // The command wrote it; the agent never saw it, and keeps it that way: the line drawn for the one who kept it is declined.
291
+ let saved;
292
+ try { saved = await send('/v1/holdings?kind=secret&name=' + encodeURIComponent(output.name), bytes, { method: 'PUT', type: 'application/octet-stream' }); }
289
293
  catch { throw new Error(recovery()); }
294
+ try { await send('/v1/relations', { relation: 'editor', object_type: 'holding', object_id: saved.holding.id }, { method: 'DELETE' }); } catch {}
290
295
  retainOutput = false;
291
296
  console.error('Saved output as ' + JSON.stringify(output.name) + '.');
292
297
  }