Skip to content

Commit 9532591

Browse files
authored
Merge branch 'main' into codex/skills-write-service-api
2 parents bb12a10 + 998f0ed commit 9532591

98 files changed

Lines changed: 2567 additions & 205 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/maturity-scorecard.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ jobs:
169169
selected_revision="$(git rev-parse HEAD)"
170170
expected_sha="${EXPECTED_SHA,,}"
171171
branch_candidate="${INPUT_REF#refs/heads/}"
172+
floating_default_branch=false
172173
trusted_reason=""
173174
174175
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
@@ -181,6 +182,12 @@ jobs:
181182
fi
182183
183184
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
185+
if [[ -z "${expected_sha// }" && "$branch_candidate" == "$DEFAULT_BRANCH" ]]; then
186+
# A direct main dispatch may wait while checkout fetches full history. Freeze the
187+
# latest fetched main here so later reusable jobs never receive a moving branch.
188+
floating_default_branch=true
189+
selected_revision="$(git rev-parse refs/remotes/origin/main)"
190+
fi
184191
185192
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
186193
trusted_reason="main-ancestor"
@@ -226,6 +233,9 @@ jobs:
226233
fi
227234
git fetch --no-tags origin "+refs/heads/${publication_base}:refs/remotes/origin/${publication_base}"
228235
publication_ref="refs/remotes/origin/${publication_base}"
236+
if [[ "$floating_default_branch" == "true" && "$publication_base" == "$DEFAULT_BRANCH" ]]; then
237+
selected_revision="$(git rev-parse "$publication_ref")"
238+
fi
229239
if ! git merge-base --is-ancestor "$selected_revision" "$publication_ref"; then
230240
echo "Ref '${INPUT_REF}' is not an ancestor of pull request base '${publication_base}'." >&2
231241
echo "Historical divergent refs remain available through artifact-only workflow calls." >&2

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ ENV GIT_COMMIT=${GIT_COMMIT} \
120120

121121
COPY . .
122122

123+
# The build stage also backs non-root live-test containers. Build contexts preserve
124+
# host modes, so normalize readability before Node resolves workspace packages.
125+
RUN chmod -R a+rX /app
126+
123127
# Normalize extension paths now so runtime COPY preserves safe modes
124128
# without adding a second full extensions layer.
125129
RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do \

docs/.generated/plugin-sdk-api-baseline.sha256

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ c0ffaed532578cf33493992e1ff806b2268b8e3774a92edbaede5cf5bda162a6 module/media-u
9494
8d8c0c4ebfc6e0c6125df3ae64ec66c10cd797a325de5ea7869fcd84068ab290 module/persistent-dedupe
9595
987648ebe317cc4d6c0505a52d344b12ce9c3a8261a5bd969cee3423c0c53529 module/plugin-config-runtime
9696
a5e4304b8b5878d5b7b7732939e1c91d009b4d3e9ee1c41d2f6ccde81c10eb99 module/plugin-entry
97-
b41a69c03c3a671a120963bebec65f379261220b72a4636242223d8bb15b830c module/plugin-runtime
97+
8097910898d08166f3e2e3676de5dac43f3442cfb1faa1b894ded353d973d09d module/plugin-runtime
9898
83df49f1fcb2fc4ac1bd2406a87a10be71bbcdb0967f61e9a6ccaa23c534149f module/provider-auth
9999
71bebeac51e701cd7c8e63d22754b9bcbd82b024aced303aa055d229781129d7 module/provider-catalog-runtime
100100
56151035047a69e6163d5578023d00f51a2413b777f3784af88e06261c039345 module/proxy-capture

docs/help/testing-live.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ Docker notes:
344344
the Gateway and physical Codex app-server, then repeats the output and
345345
compaction wave. Tune the bounded work with
346346
`OPENCLAW_LIVE_CODEX_HARNESS_COMPACTION_STRESS_TURNS` (1-8) and
347-
`OPENCLAW_LIVE_CODEX_HARNESS_LARGE_OUTPUT_BYTES` (100000-1000000).
347+
`OPENCLAW_LIVE_CODEX_HARNESS_LARGE_OUTPUT_BYTES` (100000-800000).
348348
- Optional loop-relay opt-out probe:
349349
`OPENCLAW_LIVE_CODEX_HARNESS_DISABLE_LOOP_RELAY=1`
350350
- The requested thinking preference may map to the nearest effort advertised

extensions/synology-chat/src/accounts.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
} from "openclaw/plugin-sdk/account-resolution";
1313
import { resolveDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
1414
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
15-
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
15+
import {
16+
normalizeOptionalString,
17+
normalizeStringEntries,
18+
} from "openclaw/plugin-sdk/string-coerce-runtime";
1619
import type {
1720
SynologyChatChannelConfig,
1821
ResolvedSynologyChatAccount,
@@ -127,12 +130,12 @@ export function resolveAccount(
127130
});
128131

129132
// Env var fallbacks (primarily for the "default" account)
130-
const envToken = process.env.SYNOLOGY_CHAT_TOKEN ?? "";
131-
const envIncomingUrl = process.env.SYNOLOGY_CHAT_INCOMING_URL ?? "";
132-
const envNasHost = process.env.SYNOLOGY_NAS_HOST ?? "localhost";
133-
const envAllowedUserIds = process.env.SYNOLOGY_ALLOWED_USER_IDS ?? "";
133+
const envToken = normalizeOptionalString(process.env.SYNOLOGY_CHAT_TOKEN) ?? "";
134+
const envIncomingUrl = normalizeOptionalString(process.env.SYNOLOGY_CHAT_INCOMING_URL) ?? "";
135+
const envNasHost = normalizeOptionalString(process.env.SYNOLOGY_NAS_HOST) ?? "localhost";
136+
const envAllowedUserIds = normalizeOptionalString(process.env.SYNOLOGY_ALLOWED_USER_IDS) ?? "";
134137
const envRateLimitValue = parseRateLimitPerMinute(process.env.SYNOLOGY_RATE_LIMIT);
135-
const envBotName = process.env.OPENCLAW_BOT_NAME ?? "OpenClaw";
138+
const envBotName = normalizeOptionalString(process.env.OPENCLAW_BOT_NAME) ?? "OpenClaw";
136139
const webhookPathSource = resolveWebhookPathSource({ accountId: id, channelCfg, rawAccount });
137140
const dangerouslyAllowInheritedWebhookPath =
138141
rawAccount.dangerouslyAllowInheritedWebhookPath ??

extensions/synology-chat/src/core.test.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,37 @@ describe("synology-chat account resolution", () => {
223223
});
224224

225225
it("uses env var fallbacks", () => {
226-
process.env.SYNOLOGY_CHAT_TOKEN = "env-tok";
227-
process.env.SYNOLOGY_CHAT_INCOMING_URL = "https://nas/incoming";
228-
process.env.SYNOLOGY_NAS_HOST = "192.0.2.1";
229-
process.env.OPENCLAW_BOT_NAME = "TestBot";
226+
const padded = "test-auth-token".padStart(16).padEnd(17);
227+
vi.stubEnv("SYNOLOGY_CHAT_TOKEN", padded);
228+
vi.stubEnv("SYNOLOGY_CHAT_INCOMING_URL", " https://nas/incoming ");
229+
vi.stubEnv("SYNOLOGY_NAS_HOST", " 192.0.2.1 ");
230+
vi.stubEnv("OPENCLAW_BOT_NAME", " TestBot ");
230231

231232
const cfg = { channels: { "synology-chat": {} } };
232233
const account = resolveAccount(cfg);
233-
expect(account.token).toBe("env-tok");
234+
expect(account.token).toBe("test-auth-token");
234235
expect(account.incomingUrl).toBe("https://nas/incoming");
235236
expect(account.nasHost).toBe("192.0.2.1");
236237
expect(account.botName).toBe("TestBot");
237238
});
238239

240+
it("ignores blank env var fallbacks when resolving the default account", () => {
241+
const whitespace = " ";
242+
vi.stubEnv("SYNOLOGY_CHAT_TOKEN", whitespace);
243+
vi.stubEnv("SYNOLOGY_CHAT_INCOMING_URL", whitespace);
244+
vi.stubEnv("SYNOLOGY_NAS_HOST", whitespace);
245+
vi.stubEnv("SYNOLOGY_ALLOWED_USER_IDS", whitespace);
246+
vi.stubEnv("OPENCLAW_BOT_NAME", whitespace);
247+
248+
const account = resolveAccount({ channels: { "synology-chat": {} } });
249+
250+
expect(account.token).toBe("");
251+
expect(account.incomingUrl).toBe("");
252+
expect(account.nasHost).toBe("localhost");
253+
expect(account.allowedUserIds).toEqual([]);
254+
expect(account.botName).toBe("OpenClaw");
255+
});
256+
239257
it("lets config and account overrides win over env/base config", () => {
240258
process.env.SYNOLOGY_CHAT_TOKEN = "env-tok";
241259
const cfg = {

scripts/check-plugin-sdk-subpath-exports.mjs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import {
1313
} from "./lib/ts-guard-utils.mjs";
1414

1515
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
16-
const scanRoots = resolveSourceRoots(repoRoot, ["src", "extensions", "scripts", "test"]);
16+
const scanRoots = resolveSourceRoots(repoRoot, [
17+
"src",
18+
"packages",
19+
"extensions",
20+
"scripts",
21+
"test",
22+
]);
1723

1824
function readPackageExports() {
1925
const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
@@ -49,6 +55,28 @@ function parsePluginSdkSubpath(specifier) {
4955
return subpath || null;
5056
}
5157

58+
function isGeneratedBuildArtifact(filePath) {
59+
return normalizeRepoPath(repoRoot, filePath).split("/").includes("dist");
60+
}
61+
62+
function isRuntimeModuleReference(node) {
63+
// With verbatimModuleSyntax, inline `type` specifiers emit an empty import/export and still
64+
// resolve the module. Only declaration-level `import type` and `export type` are erased.
65+
if (ts.isImportDeclaration(node)) {
66+
return !node.importClause?.isTypeOnly;
67+
}
68+
if (ts.isExportDeclaration(node)) {
69+
return !node.isTypeOnly;
70+
}
71+
if (ts.isImportTypeNode(node)) {
72+
return false;
73+
}
74+
if (ts.isImportEqualsDeclaration(node)) {
75+
return !node.isTypeOnly;
76+
}
77+
return true;
78+
}
79+
5280
function compareEntries(left, right) {
5381
return (
5482
left.file.localeCompare(right.file) ||
@@ -63,22 +91,44 @@ async function collectViolations() {
6391
const entrypoints = readEntrypoints();
6492
const exports = readPackageExports();
6593
const privateLocalOnlySubpaths = readPrivateLocalOnlySubpaths();
66-
const files = (await collectTypeScriptFilesFromRoots(scanRoots, { includeTests: true })).toSorted(
67-
(left, right) =>
68-
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
94+
// Workspace packages resolve private facades through root TS paths and bundle them into dist;
95+
// live jiti source stages inject the same private map. Core src callers must stay relative.
96+
const coreRuntimeFiles = new Set(
97+
(
98+
await collectTypeScriptFilesFromRoots(resolveSourceRoots(repoRoot, ["src"]), {
99+
includeTests: false,
100+
extraTestSuffixes: [".test-support.ts", ".test-loader.ts", ".test-fixtures.ts"],
101+
})
102+
).filter((filePath) => !isGeneratedBuildArtifact(filePath)),
69103
);
104+
const files = (await collectTypeScriptFilesFromRoots(scanRoots, { includeTests: true }))
105+
.filter((filePath) => !isGeneratedBuildArtifact(filePath))
106+
.toSorted((left, right) =>
107+
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
108+
);
70109
const violations = [];
71110

72111
for (const filePath of files) {
73112
const sourceText = readFileSync(filePath, "utf8");
74113
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
75114

76-
function push(kind, specifierNode, specifier) {
115+
function push(kind, node, specifierNode, specifier) {
77116
const subpath = parsePluginSdkSubpath(specifier);
78117
if (!subpath) {
79118
return;
80119
}
81120
if (privateLocalOnlySubpaths.has(subpath)) {
121+
const repoPath = normalizeRepoPath(repoRoot, filePath);
122+
if (coreRuntimeFiles.has(filePath) && isRuntimeModuleReference(node)) {
123+
violations.push({
124+
file: repoPath,
125+
line: toLine(sourceFile, specifierNode),
126+
kind,
127+
specifier,
128+
subpath,
129+
reason: "private runtime helper used by core must use a relative import",
130+
});
131+
}
82132
return;
83133
}
84134

@@ -99,12 +149,12 @@ async function collectViolations() {
99149
kind,
100150
specifier,
101151
subpath,
102-
missingFrom,
152+
reason: `missing from ${missingFrom.join(" and ")}`,
103153
});
104154
}
105155

106-
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifier, specifierNode }) => {
107-
push(kind, specifierNode, specifier);
156+
visitModuleSpecifiers(ts, sourceFile, ({ kind, node, specifier, specifierNode }) => {
157+
push(kind, node, specifierNode, specifier);
108158
});
109159
}
110160

@@ -119,11 +169,11 @@ async function main() {
119169
}
120170

121171
console.error(
122-
"Rule: every referenced openclaw/plugin-sdk/<subpath> must exist in the public package exports.",
172+
"Rule: every referenced openclaw/plugin-sdk/<subpath> must be public or use its required private boundary.",
123173
);
124174
for (const violation of violations) {
125175
console.error(
126-
`- ${violation.file}:${violation.line} [${violation.kind}] ${violation.specifier} missing from ${violation.missingFrom.join(" and ")}`,
176+
`- ${violation.file}:${violation.line} [${violation.kind}] ${violation.specifier}: ${violation.reason}`,
127177
);
128178
}
129179
process.exit(1);

scripts/e2e/parallels/phase-runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { appendFileSync } from "node:fs";
33
import { writeFile } from "node:fs/promises";
44
import path from "node:path";
55
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
6+
import { tailText } from "../lib/text-file-utils.mjs";
67
import { say, warn } from "./host-command.ts";
78

89
const PHASE_LOG_TAIL_MAX_BYTES = 512 * 1024;
@@ -15,8 +16,8 @@ function appendTextTail(current: string, chunk: string, maxBytes: number): strin
1516
}
1617
const marker = `[phase log tail truncated to last ${maxBytes} bytes]\n`;
1718
const tailBytes = Math.max(0, maxBytes - Buffer.byteLength(marker));
18-
const tail = Buffer.from(combined).subarray(-tailBytes).toString("utf8");
19-
return `${marker}${tail}`;
19+
// tailText owns the UTF-8-safe byte truncation; the marker keeps the tail self-describing.
20+
return `${marker}${tailText(combined, tailBytes)}`;
2021
}
2122

2223
function resolvePhaseTimeoutMs(timeoutSeconds: number): number {

scripts/lib/docker-build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ docker_build_args_need_buildx() {
5151
docker_build_transient_failure() {
5252
local log_file="$1"
5353
grep -Eqi \
54-
'frontend grpc server closed unexpectedly|failed to dial gRPC|no active session|buildkit.*connection.*closed|rpc error: code = Unavailable|failed to fetch oauth token:.*(5[0-9][0-9]|Gateway Timeout)|unexpected status from .*: 5[0-9][0-9]|TLS handshake timeout|net/http: TLS handshake timeout|i/o timeout|connection reset by peer' \
54+
'frontend grpc server closed unexpectedly|failed to dial gRPC|no active session|buildkit.*connection.*closed|rpc error: code = Unavailable|failed to fetch oauth token:.*(5[0-9][0-9]|Gateway Timeout)|unexpected status from .*: 5[0-9][0-9]|TLS handshake timeout|net/http: TLS handshake timeout|ConnectTimeoutError|Connect Timeout Error|i/o timeout|connection reset by peer' \
5555
"$log_file"
5656
}
5757

5858
docker_build_resource_exhausted_failure() {
5959
local log_file="$1"
60-
grep -Eqi 'ResourceExhausted|cannot allocate memory|out of memory|exit code: 137|signal: killed|Killed' "$log_file"
60+
grep -Eqi 'ResourceExhausted|cannot allocate memory|out of memory|exit code: 137|signal: killed|fatal error: killed signal terminated program|(^|#[0-9]+ [0-9.]+ )Killed[[:space:]]*$' "$log_file"
6161
}
6262

6363
docker_build_print_resource_exhausted_hint() {

scripts/lib/live-docker-stage.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ openclaw_live_stage_source_tree() {
3737
if [ "$status" -gt 1 ]; then
3838
return "$status"
3939
fi
40+
41+
local scripts_dir="${OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR:-/src/scripts}"
42+
node "$scripts_dir/live-docker-stage-private-sdk-exports.mjs" "$dest_dir"
4043
}
4144

4245
openclaw_live_link_runtime_tree() {

0 commit comments

Comments
 (0)