Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions lazurio/core/organization-activation-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ test("Organization settings ride next to the resource and never touch projection
status: "valid",
source: "lazurio.organization.json",
values: { integrations: { composio: { allowed: false } } },
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedMicrosoft365()],
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedAfterComposio()],
issues: [],
});
expect(plain.settings).toMatchObject({ status: "absent", source: "lazurio.organization.json", values: {} });
Expand Down Expand Up @@ -1047,12 +1047,12 @@ function fixture({ owner = true, canCreate = true, app = {}, root = {} } = {}) {
};
}

// The Microsoft 365 company app (decision 0162, addendum 2026-10-14) is not
// declared by these fixtures, so its keys stay ungoverned.
function ungovernedMicrosoft365() {
return ["tenant_id", "client_id", "apps", "callbacks"].map((field) => ({
key: `integrations.microsoft365.${field}`,
governed: false,
value: null,
}));
// Neither the Microsoft 365 company app (decision 0162, addendum 2026-10-14)
// nor the Workspace tabs (decision 0206) are declared by these fixtures, so
// their keys stay ungoverned.
function ungovernedAfterComposio() {
return [
...["tenant_id", "client_id", "apps", "callbacks"].map((field) => `integrations.microsoft365.${field}`),
"workspace.tabs",
].map((key) => ({ key, governed: false, value: null }));
}
18 changes: 9 additions & 9 deletions lazurio/core/organization-scaffold-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("Organization scaffold", () => {
expect(company).not.toHaveProperty("settings");
expect(resolution.settings).toMatchObject({
status: "valid",
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedMicrosoft365()],
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedAfterComposio()],
});
});

Expand Down Expand Up @@ -278,12 +278,12 @@ function hermeticGitEnvironment(root) {
return env;
}

// The Microsoft 365 company app (decision 0162, addendum 2026-10-14) is not
// declared by these fixtures, so its keys stay ungoverned.
function ungovernedMicrosoft365() {
return ["tenant_id", "client_id", "apps", "callbacks"].map((field) => ({
key: `integrations.microsoft365.${field}`,
governed: false,
value: null,
}));
// Neither the Microsoft 365 company app (decision 0162, addendum 2026-10-14)
// nor the Workspace tabs (decision 0206) are declared by these fixtures, so
// their keys stay ungoverned.
function ungovernedAfterComposio() {
return [
...["tenant_id", "client_id", "apps", "callbacks"].map((field) => `integrations.microsoft365.${field}`),
"workspace.tabs",
].map((key) => ({ key, governed: false, value: null }));
}
46 changes: 45 additions & 1 deletion lazurio/core/organization-settings-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ const LAUNCHPAD_CALLBACK_PATTERN =
// callback always takes one of them.
const MICROSOFT365_MAX_CALLBACKS = 255;

// An Organization's own Workspace tabs (decision 0206): 1 to 12 tabs, each a
// name of 1 to 40 characters without control characters or surrounding
// space, and the modules in it by their lazurio.module.v1 id.
// biome-ignore lint/suspicious/noControlCharactersInRegex: the control characters are what it refuses.
const WORKSPACE_TAB_NAME_PATTERN = /^(?=[^\u0000-\u001f\u007f]{1,40}$)[^\s\u0000-\u001f\u007f](?:.*[^\s\u0000-\u001f\u007f])?$/;
const WORKSPACE_MODULE_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the extra module-ID length limit

When an Organization has a module ID longer than 63 characters, that module remains valid under the canonical lazurio.module.v1 contract (lazurio/schemas/lazurio-module.schema.json:9 and validateModuleManifest impose no maximum), but this new pattern rejects any tab containing it and invalidates the entire settings section. Reuse the canonical module-ID constraint here, or change the owning module contract consistently if a 63-character limit is required.

AGENTS.md reference: AGENTS.md:L224-L226

Useful? React with 👍 / 👎.

export const WORKSPACE_TABS_MAX = 12;

const CANONICAL_SOURCE = "lazurio.organization.json";

// The whole contract as one closed tree. Adding an Organization setting adds
Expand All @@ -65,6 +73,21 @@ const SETTINGS_CONTRACT = objectNode({
["tenant_id", "client_id", "apps", "callbacks"],
),
}),
workspace: objectNode(
{
tabs: arrayLeaf(
objectNode(
{
name: stringLeaf(WORKSPACE_TAB_NAME_PATTERN),
modules: arrayLeaf(stringLeaf(WORKSPACE_MODULE_PATTERN), { maxItems: 500 }),
},
["name", "modules"],
),
{ minItems: 1, maxItems: WORKSPACE_TABS_MAX },
),
},
["tabs"],
),
});

/** Dot-separated keys of every known setting, as reported per item to the Dashboard. */
Expand Down Expand Up @@ -103,6 +126,7 @@ export function resolveOrganizationSettings({ authority, canonicalManifest }) {
}
const issues = [];
validateNode(canonicalManifest.settings, SETTINGS_CONTRACT, "/settings", issues);
if (issues.length === 0) workspaceIssues(canonicalManifest.settings, issues);
if (issues.length > 0) {
return settingsResult({
status: "invalid",
Expand Down Expand Up @@ -184,6 +208,26 @@ function validateNode(value, node, path, issues) {
}
}

// What the closed tree (and the JSON schema) cannot say about the tabs: two
// tabs never share a name, letter case ignored, and a module is in one tab
// at most.
function workspaceIssues(settings, issues) {
const tabs = settings.workspace?.tabs;
if (tabs === undefined) return;
const names = new Set();
const modules = new Set();
tabs.forEach((tab, index) => {
const at = `/settings/workspace/tabs/${index}`;
const name = tab.name.normalize("NFC").toLocaleLowerCase();
if (names.has(name)) issues.push({ code: "settings_value_invalid", path: `${at}/name` });
names.add(name);
tab.modules.forEach((module, position) => {
if (modules.has(module)) issues.push({ code: "settings_value_invalid", path: `${at}/modules/${position}` });
modules.add(module);
});
});
}

function objectNode(fields, required = []) {
return Object.freeze({
kind: "object",
Expand All @@ -205,7 +249,7 @@ function enumItem(values) {
}

// An array is one setting: its items are validated, never reported as keys.
// Items are strings and must be unique.
// String items must be unique.
function arrayLeaf(items, { minItems = 0, maxItems = Infinity } = {}) {
return Object.freeze({ kind: "array", items, minItems, maxItems });
}
Expand Down
74 changes: 67 additions & 7 deletions lazurio/core/organization-settings-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const microsoftKeys = [
"integrations.microsoft365.apps",
"integrations.microsoft365.callbacks",
];
const ungovernedMicrosoft = microsoftKeys.map((key) => ({ key, governed: false, value: null }));
// Every setting after Composio's, none of them governed.
const ungovernedAfterComposio = [...microsoftKeys, "workspace.tabs"].map((key) => ({ key, governed: false, value: null }));
const tenantId = "11111111-2222-4333-8444-aaaaaaaaaaaa";
const clientId = "11111111-2222-4333-8444-bbbbbbbbbbbb";
const callback = "https://launchpad.machine-1.example-org.lazurio.io/api/integrations/oauth/callback";
Expand All @@ -42,7 +43,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
"settings_type_invalid",
"settings_value_invalid",
]);
expect(ORGANIZATION_SETTINGS_KEYS).toEqual(["integrations.composio.allowed", ...microsoftKeys]);
expect(ORGANIZATION_SETTINGS_KEYS).toEqual(["integrations.composio.allowed", ...microsoftKeys, "workspace.tabs"]);
expect(MICROSOFT365_APPS).toEqual(["outlook", "one_drive", "share_point"]);
expect(Object.isFrozen(ORGANIZATION_SETTINGS_KEYS)).toBe(true);
});
Expand All @@ -53,7 +54,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
status: "absent",
source: canonicalSource,
values: {},
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedMicrosoft],
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedAfterComposio],
issues: [],
});
});
Expand All @@ -65,7 +66,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
status: "valid",
source: canonicalSource,
values: settings,
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedMicrosoft],
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedAfterComposio],
issues: [],
});
}
Expand All @@ -76,7 +77,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
status: "valid",
source: canonicalSource,
values: settings,
effective: [{ key: "integrations.composio.allowed", governed: true, value: allowed }, ...ungovernedMicrosoft],
effective: [{ key: "integrations.composio.allowed", governed: true, value: allowed }, ...ungovernedAfterComposio],
issues: [],
});
}
Expand Down Expand Up @@ -151,7 +152,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
status: "absent",
source: null,
values: {},
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedMicrosoft],
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedAfterComposio],
issues: [],
});
for (const input of [
Expand Down Expand Up @@ -187,7 +188,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
expect(newOrganizationSettings()).toEqual({ integrations: { composio: { allowed: false } } });
expect(settingsOf(manifest(newOrganizationSettings()))).toMatchObject({
status: "valid",
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedMicrosoft],
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedAfterComposio],
});
});

Expand All @@ -204,6 +205,7 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
{ key: "integrations.microsoft365.client_id", governed: true, value: clientId },
{ key: "integrations.microsoft365.apps", governed: true, value: ["outlook", "one_drive", "share_point"] },
{ key: "integrations.microsoft365.callbacks", governed: true, value: [callback] },
{ key: "workspace.tabs", governed: false, value: null },
],
issues: [],
});
Expand Down Expand Up @@ -309,6 +311,17 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
{ integrations: { microsoft365: { ...microsoft365, callbacks: ["https://attacker.example/api/integrations/oauth/callback"] } } },
{ integrations: { microsoft365: { ...microsoft365, callbacks: manyCallbacks(256) } } },
{ integrations: { microsoft365: { ...microsoft365, client_secret: "value" } } },
{ workspace: { tabs } },
{ workspace: {} },
{ workspace: { tabs: [] } },
{ workspace: { tabs: Array.from({ length: 13 }, (_, index) => ({ name: `T${index}`, modules: [] })) } },
{ workspace: { tabs: [{ name: " Finance", modules: [] }] } },
{ workspace: { tabs: [{ name: "x".repeat(41), modules: [] }] } },
{ workspace: { tabs: [{ name: "Fi\nnance", modules: [] }] } },
{ workspace: { tabs: [{ name: "Finance", modules: ["../deals"] }] } },
{ workspace: { tabs: [{ name: "Finance", modules: ["deals", "deals"] }] } },
{ workspace: { tabs: [{ name: "Finance" }] } },
{ workspace: { tabs: [{ name: "Finance", modules: [], icon: "x" }] } },
];
for (const settings of documents) {
const coreValid = settingsOf(manifest(settings)).status === "valid";
Expand All @@ -318,6 +331,53 @@ describe("Organization settings contract (decision 0194, DEV-6653 C1)", () => {
});
});

// Decision 0206: an Organization's own Workspace tabs, one setting.
const tabs = [
{ name: "Každodenní práce", modules: ["deals", "knowledgebase"] },
{ name: "Finance", modules: ["invoices"] },
{ name: "Sklad", modules: [] },
];

describe("Workspace tabs (decision 0206)", () => {
test("are one setting: ordered names, each module in one tab at most", () => {
expect(settingsOf(manifest({ workspace: { tabs } }))).toMatchObject({
status: "valid",
values: { workspace: { tabs } },
});
expect(settingsOf(manifest({ workspace: { tabs } })).effective.at(-1)).toEqual({
key: "workspace.tabs",
governed: true,
value: tabs,
});
// What the JSON schema cannot express, Core refuses on its own.
const twice = [
[
[
{ name: "Finance", modules: ["deals"] },
{ name: "FINANCE", modules: [] },
],
"/settings/workspace/tabs/1/name",
],
[
[
{ name: "Finance", modules: ["deals"] },
{ name: "Sklad", modules: ["deals"] },
],
"/settings/workspace/tabs/1/modules/0",
],
];
for (const [value, path] of twice) {
expect(settingsOf(manifest({ workspace: { tabs: value } }))).toMatchObject({
status: "invalid",
values: null,
issues: [issue("settings_value_invalid", path)],
});
expect(validateAgainstSchema(manifest({ workspace: { tabs: value } }), organizationManifestSchema, "organization"))
.toEqual([]);
}
});
});

function settingsOf(canonicalManifest) {
return resolveOrganizationSettings({ authority: "canonical", canonicalManifest });
}
Expand Down
33 changes: 33 additions & 0 deletions lazurio/lazurio.organization.v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,39 @@
}
}
}
},
"workspace": {
"type": "object",
"additionalProperties": false,
"required": ["tabs"],
"description": "The Organization's own tabs in Apps' Workspace (decision 0206), the same for every member; a module named in no tab is in the first. Core also refuses two tabs of one name (letter case ignored) and a module in two tabs, which this schema cannot express.",
"properties": {
"tabs": {
"type": "array",
"minItems": 1,
"maxItems": 12,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "modules"],
"properties": {
"name": {
"type": "string",
"pattern": "^(?=[^\\u0000-\\u001f\\u007f]{1,40}$)[^\\s\\u0000-\\u001f\\u007f](?:.*[^\\s\\u0000-\\u001f\\u007f])?$",
"description": "The tab's name: 1 to 40 characters, no control character, no space at either end."
},
"modules": {
"type": "array",
"maxItems": 500,
"uniqueItems": true,
"items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$" },
"description": "The modules of the tab, by their lazurio.module.v1 id."
}
}
},
"description": "The tabs, in their order."
}
}
}
}
},
Expand Down
20 changes: 10 additions & 10 deletions lazurio/organization-check-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test("a root with valid settings passes and prints the effective values without
status: "valid",
source: "lazurio.organization.json",
values: { integrations: { composio: { allowed: false } } },
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedMicrosoft365()],
effective: [{ key: "integrations.composio.allowed", governed: true, value: false }, ...ungovernedAfterComposio()],
issues: [],
},
failures: [],
Expand All @@ -66,7 +66,7 @@ test("a root without settings passes: the Organization governs nothing", () => {
settings: {
status: "absent",
values: {},
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedMicrosoft365()],
effective: [{ key: "integrations.composio.allowed", governed: false, value: null }, ...ungovernedAfterComposio()],
},
});
expect(run(["organization", "check", root]).stdout).toContain("integrations.composio.allowed: neřízeno");
Expand Down Expand Up @@ -255,12 +255,12 @@ function run(args) {
};
}

// The Microsoft 365 company app (decision 0162, addendum 2026-10-14) is not
// declared by these fixtures, so its keys stay ungoverned.
function ungovernedMicrosoft365() {
return ["tenant_id", "client_id", "apps", "callbacks"].map((field) => ({
key: `integrations.microsoft365.${field}`,
governed: false,
value: null,
}));
// Neither the Microsoft 365 company app (decision 0162, addendum 2026-10-14)
// nor the Workspace tabs (decision 0206) are declared by these fixtures, so
// their keys stay ungoverned.
function ungovernedAfterComposio() {
return [
...["tenant_id", "client_id", "apps", "callbacks"].map((field) => `integrations.microsoft365.${field}`),
"workspace.tabs",
].map((key) => ({ key, governed: false, value: null }));
}
Loading
Loading