@tpsdev-ai/flair 0.8.2 → 0.9.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/README.md +27 -0
- package/dist/cli.js +380 -5
- package/dist/rem/restore.js +205 -0
- package/dist/rem/runner.js +221 -0
- package/dist/rem/scheduler.js +240 -0
- package/dist/rem/snapshot.js +139 -0
- package/dist/resources/A2AAdapter.js +5 -0
- package/dist/resources/Admin.js +16 -0
- package/dist/resources/AdminInstance.js +50 -4
- package/dist/resources/AdminMemory.js +7 -1
- package/dist/resources/MemoryMaintenance.js +50 -22
- package/dist/resources/OAuth.js +22 -0
- package/dist/resources/ObservationCenter.js +4 -0
- package/dist/resources/SkillScan.js +32 -89
- package/dist/resources/admin-layout.js +25 -1
- package/dist/resources/auth-middleware.js +14 -0
- package/dist/resources/health.js +20 -1
- package/dist/resources/scan/skill-scanner.js +166 -0
- package/package.json +2 -1
- package/templates/bin/flair-rem-nightly.sh.tmpl +9 -0
- package/templates/launchd/dev.flair.rem.nightly.plist.tmpl +46 -0
- package/templates/systemd/flair-rem-nightly.service.tmpl +14 -0
- package/templates/systemd/flair-rem-nightly.timer.tmpl +10 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill content static analyzer — pure, no Harper runtime deps.
|
|
3
|
+
*
|
|
4
|
+
* Imported by `resources/SkillScan.ts` (the HTTP Resource) and by unit
|
|
5
|
+
* tests in `test/unit/SkillScan.test.ts`. Keeping the scanner logic in a
|
|
6
|
+
* separate module lets the tests run without instantiating the Harper
|
|
7
|
+
* runtime.
|
|
8
|
+
*
|
|
9
|
+
* See SkillScan.ts for the design rationale on markdown awareness and the
|
|
10
|
+
* language-agnostic vs shell-only pattern split.
|
|
11
|
+
*/
|
|
12
|
+
const SHELL_PATTERNS = [
|
|
13
|
+
{ regex: /\bexec\s*\(/, type: "shell_command" },
|
|
14
|
+
{ regex: /\bspawn\s*\(/, type: "shell_command" },
|
|
15
|
+
{ regex: /\bsystem\s*\(/, type: "shell_command" },
|
|
16
|
+
{ regex: /\bchild_process\b/, type: "shell_command" },
|
|
17
|
+
];
|
|
18
|
+
const NETWORK_PATTERNS = [
|
|
19
|
+
{ regex: /\bfetch\s*\(/, type: "network_call" },
|
|
20
|
+
{ regex: /\bcurl\b/, type: "network_call" },
|
|
21
|
+
{ regex: /https?:\/\//, type: "url_reference" },
|
|
22
|
+
{ regex: /\bXMLHttpRequest\b/, type: "network_call" },
|
|
23
|
+
{ regex: /\baxios\b/, type: "network_call" },
|
|
24
|
+
];
|
|
25
|
+
const FS_PATTERNS = [
|
|
26
|
+
{ regex: /\bfs\.write/, type: "fs_write" },
|
|
27
|
+
{ regex: /\bwriteFile/, type: "fs_write" },
|
|
28
|
+
{ regex: />[>]?\s*[\/~]/, type: "fs_redirect" },
|
|
29
|
+
];
|
|
30
|
+
const ENV_PATTERNS = [
|
|
31
|
+
{ regex: /\bprocess\.env\b/, type: "env_access" },
|
|
32
|
+
{ regex: /\$ENV\b/, type: "env_access" },
|
|
33
|
+
{ regex: /\$\{?\w+\}?/, type: "env_variable" },
|
|
34
|
+
];
|
|
35
|
+
const ENCODING_PATTERNS = [
|
|
36
|
+
{ regex: /\batob\s*\(/, type: "base64_decode" },
|
|
37
|
+
{ regex: /\bbtoa\s*\(/, type: "base64_encode" },
|
|
38
|
+
{ regex: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]/, type: "base64_decode" },
|
|
39
|
+
{ regex: /Buffer\.from\s*\([^)]*,\s*['"]hex['"]/, type: "hex_decode" },
|
|
40
|
+
{ regex: /\\x[0-9a-fA-F]{2}/, type: "hex_escape" },
|
|
41
|
+
{ regex: /\\u200[b-f]|\\u2060|\\ufeff/, type: "zero_width_char" },
|
|
42
|
+
];
|
|
43
|
+
const UNICODE_PATTERNS = [
|
|
44
|
+
{ regex: /[-]/, type: "zero_width_char" },
|
|
45
|
+
{ regex: /[А-я]/, type: "cyrillic_homoglyph" },
|
|
46
|
+
];
|
|
47
|
+
const LANG_AGNOSTIC_PATTERNS = [
|
|
48
|
+
...NETWORK_PATTERNS,
|
|
49
|
+
...FS_PATTERNS,
|
|
50
|
+
...ENV_PATTERNS,
|
|
51
|
+
...ENCODING_PATTERNS,
|
|
52
|
+
...UNICODE_PATTERNS,
|
|
53
|
+
];
|
|
54
|
+
const SHELL_FENCE_LANGS = new Set(["", "sh", "bash", "shell", "zsh"]);
|
|
55
|
+
const MARKDOWN_IDENTIFIER_RE = /^[\w@./-]+$/;
|
|
56
|
+
const SHELL_BACKTICK_INDICATORS = [
|
|
57
|
+
/\s/,
|
|
58
|
+
/[|;&]/,
|
|
59
|
+
/\$\(/,
|
|
60
|
+
/\$\{/,
|
|
61
|
+
/^\$\w/,
|
|
62
|
+
/>/,
|
|
63
|
+
/<\(/,
|
|
64
|
+
];
|
|
65
|
+
function lineHasShellishBacktick(line) {
|
|
66
|
+
const matches = line.match(/`([^`\n]+)`/g);
|
|
67
|
+
if (!matches)
|
|
68
|
+
return false;
|
|
69
|
+
for (const raw of matches) {
|
|
70
|
+
const inner = raw.slice(1, -1);
|
|
71
|
+
if (MARKDOWN_IDENTIFIER_RE.test(inner))
|
|
72
|
+
continue;
|
|
73
|
+
for (const indicator of SHELL_BACKTICK_INDICATORS) {
|
|
74
|
+
if (indicator.test(inner))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function assessRisk(violations) {
|
|
81
|
+
if (violations.length === 0)
|
|
82
|
+
return "low";
|
|
83
|
+
const types = new Set(violations.map((v) => v.type));
|
|
84
|
+
// Programmatic shell call patterns: exec/spawn/system/child_process. These
|
|
85
|
+
// are definite payloads when they appear in a skill.
|
|
86
|
+
const hasShellCommand = types.has("shell_command");
|
|
87
|
+
// Backticked shell-ish content in markdown prose. Could be `npm run deploy`
|
|
88
|
+
// in documentation (medium), or could combine with other smells (high).
|
|
89
|
+
const hasShellBacktick = types.has("shell_backtick");
|
|
90
|
+
const hasFs = types.has("fs_write") || types.has("fs_redirect");
|
|
91
|
+
const hasEncoding = types.has("base64_decode") || types.has("hex_decode");
|
|
92
|
+
const hasZeroWidth = types.has("zero_width_char");
|
|
93
|
+
const hasHomoglyph = types.has("cyrillic_homoglyph");
|
|
94
|
+
// Critical: any shell combined with encoded payloads, OR obfuscation chars.
|
|
95
|
+
if (((hasShellCommand || hasShellBacktick) && hasEncoding) ||
|
|
96
|
+
hasZeroWidth ||
|
|
97
|
+
hasHomoglyph) {
|
|
98
|
+
return "critical";
|
|
99
|
+
}
|
|
100
|
+
// High: programmatic shell, fs writes, or shell-backtick + other smells
|
|
101
|
+
// (env access, network call). A skill that quotes a command AND reads env
|
|
102
|
+
// AND fetches a URL is doing something, not just documenting.
|
|
103
|
+
const hasOtherSmells = types.has("env_access") ||
|
|
104
|
+
types.has("env_variable") ||
|
|
105
|
+
types.has("network_call") ||
|
|
106
|
+
types.has("url_reference");
|
|
107
|
+
if (hasShellCommand || hasFs || (hasShellBacktick && hasOtherSmells)) {
|
|
108
|
+
return "high";
|
|
109
|
+
}
|
|
110
|
+
// Medium: shell_backtick alone (doc reference), or network/encoding
|
|
111
|
+
// without shell. Reviewable but not blocked.
|
|
112
|
+
if (hasShellBacktick || hasOtherSmells || hasEncoding) {
|
|
113
|
+
return "medium";
|
|
114
|
+
}
|
|
115
|
+
return "low";
|
|
116
|
+
}
|
|
117
|
+
function detectFenceTransition(line, state) {
|
|
118
|
+
const m = line.match(/^[ \t]*```([\w.-]*)\s*$/);
|
|
119
|
+
if (!m)
|
|
120
|
+
return false;
|
|
121
|
+
if (state.inFence) {
|
|
122
|
+
state.inFence = false;
|
|
123
|
+
state.lang = "";
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
state.inFence = true;
|
|
127
|
+
state.lang = (m[1] || "").toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
export function scanSkillContent(content) {
|
|
132
|
+
const lines = content.split("\n");
|
|
133
|
+
const violations = [];
|
|
134
|
+
const fence = { inFence: false, lang: "" };
|
|
135
|
+
const recordIfMatch = (lineIndex, line, patterns) => {
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
if (pattern.regex.test(line)) {
|
|
138
|
+
violations.push({
|
|
139
|
+
type: pattern.type,
|
|
140
|
+
line: lineIndex + 1,
|
|
141
|
+
content: line.trim().slice(0, 200),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i] ?? "";
|
|
148
|
+
if (detectFenceTransition(line, fence))
|
|
149
|
+
continue;
|
|
150
|
+
const insideShellFence = fence.inFence && SHELL_FENCE_LANGS.has(fence.lang);
|
|
151
|
+
const insideNonShellFence = fence.inFence && !insideShellFence;
|
|
152
|
+
if (!fence.inFence && lineHasShellishBacktick(line)) {
|
|
153
|
+
violations.push({
|
|
154
|
+
type: "shell_backtick",
|
|
155
|
+
line: i + 1,
|
|
156
|
+
content: line.trim().slice(0, 200),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (!insideNonShellFence) {
|
|
160
|
+
recordIfMatch(i, line, SHELL_PATTERNS);
|
|
161
|
+
}
|
|
162
|
+
recordIfMatch(i, line, LANG_AGNOSTIC_PATTERNS);
|
|
163
|
+
}
|
|
164
|
+
const riskLevel = assessRisk(violations);
|
|
165
|
+
return { safe: violations.length === 0, violations, riskLevel };
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"dist/",
|
|
32
32
|
"schemas/",
|
|
33
|
+
"templates/",
|
|
33
34
|
"ui/",
|
|
34
35
|
"config.yaml",
|
|
35
36
|
"LICENSE",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Flair REM nightly runner shim.
|
|
3
|
+
# Deployed by `flair rem nightly enable` to {{SHIM_PATH}}.
|
|
4
|
+
# Invoked by launchd (macOS) or systemd (Linux) on the configured schedule.
|
|
5
|
+
#
|
|
6
|
+
# Single line that invokes the CLI's run-once subcommand — the runner module
|
|
7
|
+
# does all the work. Logs land in {{HOME}}/.flair/logs/.
|
|
8
|
+
set -e
|
|
9
|
+
exec {{FLAIR_BIN}} rem nightly run-once
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
3
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4
|
+
<plist version="1.0">
|
|
5
|
+
<dict>
|
|
6
|
+
<key>Label</key>
|
|
7
|
+
<string>dev.flair.rem.nightly</string>
|
|
8
|
+
|
|
9
|
+
<key>ProgramArguments</key>
|
|
10
|
+
<array>
|
|
11
|
+
<string>{{SHIM_PATH}}</string>
|
|
12
|
+
</array>
|
|
13
|
+
|
|
14
|
+
<key>StartCalendarInterval</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>Hour</key>
|
|
17
|
+
<integer>{{HOUR}}</integer>
|
|
18
|
+
<key>Minute</key>
|
|
19
|
+
<integer>{{MINUTE}}</integer>
|
|
20
|
+
</dict>
|
|
21
|
+
|
|
22
|
+
<key>RunAtLoad</key>
|
|
23
|
+
<false/>
|
|
24
|
+
|
|
25
|
+
<key>StandardOutPath</key>
|
|
26
|
+
<string>{{HOME}}/.flair/logs/rem-nightly.stdout.log</string>
|
|
27
|
+
|
|
28
|
+
<key>StandardErrorPath</key>
|
|
29
|
+
<string>{{HOME}}/.flair/logs/rem-nightly.stderr.log</string>
|
|
30
|
+
|
|
31
|
+
<key>WorkingDirectory</key>
|
|
32
|
+
<string>{{HOME}}</string>
|
|
33
|
+
|
|
34
|
+
<key>EnvironmentVariables</key>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>HOME</key>
|
|
37
|
+
<string>{{HOME}}</string>
|
|
38
|
+
<key>PATH</key>
|
|
39
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
40
|
+
<key>FLAIR_AGENT_ID</key>
|
|
41
|
+
<string>{{AGENT_ID}}</string>
|
|
42
|
+
<key>FLAIR_URL</key>
|
|
43
|
+
<string>{{FLAIR_URL}}</string>
|
|
44
|
+
</dict>
|
|
45
|
+
</dict>
|
|
46
|
+
</plist>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Flair REM nightly cycle ({{AGENT_ID}})
|
|
3
|
+
After=network.target
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=oneshot
|
|
7
|
+
ExecStart={{SHIM_PATH}}
|
|
8
|
+
Environment=FLAIR_AGENT_ID={{AGENT_ID}}
|
|
9
|
+
Environment=FLAIR_URL={{FLAIR_URL}}
|
|
10
|
+
StandardOutput=append:{{HOME}}/.flair/logs/rem-nightly.stdout.log
|
|
11
|
+
StandardError=append:{{HOME}}/.flair/logs/rem-nightly.stderr.log
|
|
12
|
+
|
|
13
|
+
[Install]
|
|
14
|
+
WantedBy=default.target
|