@skillcdn/console 0.1.7 → 0.1.8
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/README.md +1 -1
- package/SKILLCDN.md +12 -6
- package/docs/specs/rest.md +1 -0
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +43 -20
- package/skills/building-a-console/references/the-default-console.md +140 -0
- package/skills/deploying-a-console/SKILL.md +130 -0
- package/skills/deploying-a-console/assets/deployer-policy.json +118 -0
- package/skills/deploying-a-console/assets/secrets.example.json +11 -0
- package/skills/deploying-a-console/references/access.md +78 -0
- package/skills/deploying-a-console/references/resources.md +516 -0
- package/skills/deploying-a-console/references/sizing.md +90 -0
- package/skills/deploying-a-console/scripts/import-aws-key.mjs +35 -0
- package/skills/deploying-a-console/scripts/store-secrets.mjs +114 -0
- package/skills/working-the-board/SKILL.md +11 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Writes the access key of a downloaded IAM key file into a profile of the AWS CLI's own
|
|
2
|
+
// credentials file, and deletes the key file. Neither value touches a command line, a message
|
|
3
|
+
// or the standard output. Usage: node import-aws-key.mjs <key file> <profile>
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
|
|
9
|
+
const [file, profile = "console"] = process.argv.slice(2);
|
|
10
|
+
if (file === undefined) {
|
|
11
|
+
process.stderr.write("usage: node import-aws-key.mjs <key file> <profile>\n");
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
const [header, row] = readFileSync(file, "utf8").trim().split(/\r?\n/);
|
|
15
|
+
const names = (header ?? "").split(",").map((name) => name.trim().toLowerCase());
|
|
16
|
+
const values = (row ?? "").split(",").map((value) => value.trim());
|
|
17
|
+
const id = values[names.findIndex((name) => name.includes("access key id"))];
|
|
18
|
+
const secret = values[names.findIndex((name) => name.includes("secret access key"))];
|
|
19
|
+
if (id === undefined || secret === undefined || id.length === 0 || secret.length === 0) {
|
|
20
|
+
process.stderr.write("the file does not look like a downloaded access key (.csv)\n");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const directory = join(homedir(), ".aws");
|
|
24
|
+
mkdirSync(directory, { recursive: true });
|
|
25
|
+
const credentials = join(directory, "credentials");
|
|
26
|
+
const before = existsSync(credentials)
|
|
27
|
+
? readFileSync(credentials, "utf8").replace(new RegExp(`\\[${profile}\\][^[]*`), "")
|
|
28
|
+
: "";
|
|
29
|
+
writeFileSync(
|
|
30
|
+
credentials,
|
|
31
|
+
`${before.trimEnd()}\n\n[${profile}]\naws_access_key_id = ${id}\naws_secret_access_key = ${secret}\n`.trimStart(),
|
|
32
|
+
{ mode: 0o600 },
|
|
33
|
+
);
|
|
34
|
+
unlinkSync(file);
|
|
35
|
+
process.stdout.write(`profile ${profile} written; ${file} deleted\n`);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The secrets file of a run (the shape of assets/secrets.example.json), kept outside any
|
|
2
|
+
// repository, and the parameter store: values go in from standard input or from a command's
|
|
3
|
+
// answer, and out into parameters, without ever passing through a command line or the output.
|
|
4
|
+
//
|
|
5
|
+
// node store-secrets.mjs set <file> <NAME> the value is standard input
|
|
6
|
+
// node store-secrets.mjs derive <file> <NAME> <template> {{OTHER}} in the template is replaced
|
|
7
|
+
// node store-secrets.mjs render <file> <in> <out> a JSON file with {{NAME}} placeholders filled
|
|
8
|
+
// node store-secrets.mjs key <file> <iam user> creates an access key for the user and stores both halves
|
|
9
|
+
// node store-secrets.mjs store <file> <prefix> every value as a SecureString parameter <prefix>/<NAME>
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import process from "node:process";
|
|
15
|
+
|
|
16
|
+
const [mode, file, ...rest] = process.argv.slice(2);
|
|
17
|
+
const usage = () => {
|
|
18
|
+
process.stderr.write("usage: node store-secrets.mjs set|derive|render|key|store <file> ...\n");
|
|
19
|
+
process.exit(2);
|
|
20
|
+
};
|
|
21
|
+
if (mode === undefined || file === undefined) {
|
|
22
|
+
usage();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const read = () => (existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {});
|
|
26
|
+
const write = (data) => writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
27
|
+
const fill = (data, text) =>
|
|
28
|
+
text.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, name) => {
|
|
29
|
+
if (typeof data[name] !== "string") {
|
|
30
|
+
throw new Error(`${name} is not in the secrets file`);
|
|
31
|
+
}
|
|
32
|
+
return data[name];
|
|
33
|
+
});
|
|
34
|
+
/** Runs the AWS CLI with a request body in a temporary file, so that nothing is on the command line. */
|
|
35
|
+
const aws = (args, body) => {
|
|
36
|
+
const directory = mkdtempSync(join(tmpdir(), "console-"));
|
|
37
|
+
try {
|
|
38
|
+
const request = join(directory, "request.json");
|
|
39
|
+
writeFileSync(request, JSON.stringify(body), { mode: 0o600 });
|
|
40
|
+
return execFileSync("aws", [...args, "--cli-input-json", `file://${request}`], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(directory, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
switch (mode) {
|
|
50
|
+
case "set": {
|
|
51
|
+
const [name] = rest;
|
|
52
|
+
if (name === undefined) {
|
|
53
|
+
usage();
|
|
54
|
+
}
|
|
55
|
+
const value = readFileSync(0, "utf8").replace(/\r?\n$/, "");
|
|
56
|
+
write({ ...read(), [name]: value });
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "derive": {
|
|
60
|
+
const [name, template] = rest;
|
|
61
|
+
if (name === undefined || template === undefined) {
|
|
62
|
+
usage();
|
|
63
|
+
}
|
|
64
|
+
const data = read();
|
|
65
|
+
write({ ...data, [name]: fill(data, template) });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "render": {
|
|
69
|
+
const [input, output] = rest;
|
|
70
|
+
if (input === undefined || output === undefined) {
|
|
71
|
+
usage();
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(output, fill(read(), readFileSync(input, "utf8")), { mode: 0o600 });
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "key": {
|
|
77
|
+
const [user] = rest;
|
|
78
|
+
if (user === undefined) {
|
|
79
|
+
usage();
|
|
80
|
+
}
|
|
81
|
+
const answer = JSON.parse(aws(["iam", "create-access-key"], { UserName: user }));
|
|
82
|
+
write({
|
|
83
|
+
...read(),
|
|
84
|
+
S3_ACCESS_KEY_ID: answer.AccessKey.AccessKeyId,
|
|
85
|
+
S3_SECRET_ACCESS_KEY: answer.AccessKey.SecretAccessKey,
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "store": {
|
|
90
|
+
const [prefix] = rest;
|
|
91
|
+
if (prefix === undefined) {
|
|
92
|
+
usage();
|
|
93
|
+
}
|
|
94
|
+
const data = read();
|
|
95
|
+
for (const [name, value] of Object.entries(data)) {
|
|
96
|
+
// The password lives inside the URL; a value the person deferred is not a parameter.
|
|
97
|
+
if (name === "_" || name === "DATABASE_PASSWORD" || value === "later") {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const tier = Buffer.byteLength(value, "utf8") > 4000 ? "Advanced" : "Standard";
|
|
101
|
+
aws(["ssm", "put-parameter"], {
|
|
102
|
+
Name: `${prefix}/${name}`,
|
|
103
|
+
Type: "SecureString",
|
|
104
|
+
Value: value,
|
|
105
|
+
Tier: tier,
|
|
106
|
+
Overwrite: true,
|
|
107
|
+
});
|
|
108
|
+
process.stdout.write(`${prefix}/${name} stored (${tier})\n`);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
usage();
|
|
114
|
+
}
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: working-the-board
|
|
3
|
-
description: "Works a task from the board of a SkillCDN Console with the console command, as the person whose token the command holds: reads the task, takes it, reports at the milestones of the work, asks on the board when a person must decide and waits for the answer, hands in what was made as a link or a file, writes down what the next agent needs on a page of the project, and finishes. Use when an agent is told to take, work or finish a task on the console, or to report, ask, hand in or write a page there; any agent with a shell where the command is signed in. To build a console or a script against the console's API, use building-a-console."
|
|
3
|
+
description: "Works a task from the board of a SkillCDN Console with the console command, as the person whose token the command holds: reads the task, takes it, reports at the milestones of the work, asks on the board when a person must decide and waits for the answer, hands in what was made as a link or a file, writes down what the next agent needs on a page of the project, and finishes. Use when an agent is told to take, work or finish a task on the console, or to report, ask, hand in or write a page there; any agent with a shell where the command is signed in. To build a console or a script against the console's API, use building-a-console; to deploy a console for an organization, use deploying-a-console."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.1"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
12
12
|
title: 콘솔 보드에서 일하기
|
|
13
|
-
description: "SkillCDN 콘솔의 보드에 있는 작업을 console 명령으로 수행합니다. 명령에 로그인한 사람의 자격으로, 작업을 읽고 맡고, 진행의 이정표마다 보고하고, 사람이 정해야 할 일은 보드에 물어 답을 기다리고, 만든 것을 링크나 파일로 제출하고, 다음 에이전트에게 필요한 것을 프로젝트 페이지에 적고, 마칩니다. 에이전트에게 콘솔의 작업을 맡거나 끝내라고, 또는 보고·질문·제출·페이지 쓰기를 하라고 할 때 쓰세요. 콘솔이나 API 클라이언트를 만들 때는 building-a-console을 쓰세요."
|
|
13
|
+
description: "SkillCDN 콘솔의 보드에 있는 작업을 console 명령으로 수행합니다. 명령에 로그인한 사람의 자격으로, 작업을 읽고 맡고, 진행의 이정표마다 보고하고, 사람이 정해야 할 일은 보드에 물어 답을 기다리고, 만든 것을 링크나 파일로 제출하고, 다음 에이전트에게 필요한 것을 프로젝트 페이지에 적고, 마칩니다. 에이전트에게 콘솔의 작업을 맡거나 끝내라고, 또는 보고·질문·제출·페이지 쓰기를 하라고 할 때 쓰세요. 콘솔이나 API 클라이언트를 만들 때는 building-a-console을, 조직의 콘솔을 배포할 때는 deploying-a-console을 쓰세요."
|
|
14
14
|
---
|
|
15
15
|
# Working a task on the board
|
|
16
16
|
|
|
17
17
|
A task of the board goes in, named by its number; a run comes out: the task taken, the work reported at its milestones, a decision raised and waited for where a person had to choose, what was made handed in, what was learned written down for the next agent, and the run finished. The board is a SkillCDN Console ([architecture](/docs/architecture.md)): projects of tasks, the agents at work on them, and the decisions that wait for a person. The agent acts as the person who signed the command in, on that person's standing and no more.
|
|
18
18
|
|
|
19
|
+
## How the person is involved
|
|
20
|
+
|
|
21
|
+
- **Questions:** only which task, when the request names none: "Which task should I take? Its number, or say that any ready one may be." Nothing else is asked in chat: what the task leaves open is asked on the board, where its people answer.
|
|
22
|
+
- **Checkpoints:** on the board, as decisions (phase 4), which a person answers from wherever they are; the reports say what was done at each milestone, and need no answer.
|
|
23
|
+
- **Go-ahead:** the task's body is the go-ahead. A task that says what the agent may decide alone is followed within that; everything else that would change scope, cost or a trade-off is a decision.
|
|
24
|
+
- **Changes mid-run:** a person changes the task or answers a decision on the board; the run takes it from there and says so in its next report. A report is never rewritten.
|
|
25
|
+
- **Language:** reports, questions, pages and summaries are in the language the task is written in, so that the people of the board read them as they wrote.
|
|
26
|
+
|
|
19
27
|
## Requirements
|
|
20
28
|
|
|
21
29
|
| Need | How | When missing |
|